Преглед на файлове

More UI layout optimizations.

sgrenier преди 12 години
родител
ревизия
a78311bd78

+ 18 - 13
gameplay/src/CheckBox.cpp

@@ -49,7 +49,7 @@ void CheckBox::setChecked(bool checked)
     if (_checked != checked)
     {
         _checked = checked;
-        setDirty(DIRTY_BOUNDS);
+        setDirty(DIRTY_STATE);
         notifyListeners(Control::Listener::VALUE_CHANGED);
     }
 }
@@ -87,11 +87,19 @@ void CheckBox::controlEvent(Control::Listener::EventType evt)
     }
 }
 
-void CheckBox::updateBounds(const Vector2& offset)
+void CheckBox::updateState(State state)
 {
-    Label::updateBounds(offset);
+    Label::updateState(state);
 
-    Control::State state = getState();
+    _image = getImage(_checked ? "checked" : "unchecked", state);
+}
+
+bool CheckBox::updateBounds(const Vector2& offset)
+{
+    bool changed = Label::updateBounds(offset);
+
+    // Update bounds based on normal state only
+    Control::State state = NORMAL;
 
     Vector2 size;
     if (_checked)
@@ -105,6 +113,8 @@ void CheckBox::updateBounds(const Vector2& offset)
         size.set(unselectedRegion.width, unselectedRegion.height);
     }
 
+    Rectangle oldBounds(_bounds);
+
     if (_autoSize & AUTO_SIZE_HEIGHT)
     {
         // Text-only width was already measured in Label::update - append image
@@ -119,16 +129,11 @@ void CheckBox::updateBounds(const Vector2& offset)
         setWidthInternal(_viewportBounds.height + 5 + _bounds.width);
     }
 
+    changed = changed || (_bounds != oldBounds);
+
     _textBounds.x += _viewportBounds.height + 5;
-    
-    if (_checked)
-    {
-        _image = getImage("checked", state);
-    }
-    else
-    {
-        _image = getImage("unchecked", state);
-    }
+
+    return changed;
 }
 
 unsigned int CheckBox::drawImages(Form* form, const Rectangle& clip)

+ 6 - 1
gameplay/src/CheckBox.h

@@ -105,10 +105,15 @@ protected:
      */
     void controlEvent(Control::Listener::EventType evt);
 
+    /**
+     * @see Control::updateState
+     */
+    void updateState(State state);
+
     /**
      * @see Control::updateBounds
      */
-    void updateBounds(const Vector2& offset);
+    bool updateBounds(const Vector2& offset);
 
     /**
      * @see Control::drawImages

+ 86 - 31
gameplay/src/Container.cpp

@@ -57,7 +57,7 @@ Container::Container()
       _scrollingMouseVertically(false), _scrollingMouseHorizontally(false),
       _scrollBarOpacityClip(NULL), _zIndexDefault(0),
       _selectButtonDown(false), _lastFrameTime(0), _totalWidth(0), _totalHeight(0),
-      _initializedWithScroll(false), _scrollWheelRequiresFocus(false)
+      _initializedWithScroll(false), _scrollWheelRequiresFocus(false), _inRelayout(false)
 {
 	clearContacts();
 }
@@ -374,7 +374,7 @@ void Container::setScroll(Scroll scroll)
             _canFocus = true;
         }
 
-        setDirty(DIRTY_BOUNDS);
+        setDirty(DIRTY_BOUNDS | DIRTY_STATE);
     }
 }
 
@@ -388,7 +388,7 @@ void Container::setScrollBarsAutoHide(bool autoHide)
     if (autoHide != _scrollBarsAutoHide)
     {
         _scrollBarsAutoHide = autoHide;
-        setDirty(DIRTY_BOUNDS);
+        setDirty(DIRTY_BOUNDS | DIRTY_STATE);
     }
 }
 
@@ -501,52 +501,75 @@ void Container::setActiveControl(Control* control)
     }
 }
 
+void Container::setChildrenDirty(int bits, bool recursive)
+{
+    for (size_t i = 0, count = _controls.size(); i < count; ++i)
+    {
+        Control* ctrl = _controls[i];
+        ctrl->setDirty(bits);
+        if (recursive && ctrl->isContainer())
+            static_cast<Container*>(ctrl)->setChildrenDirty(bits, true);
+    }
+}
+
 void Container::update(float elapsedTime)
 {
     Control::update(elapsedTime);
 
     for (size_t i = 0, count = _controls.size(); i < count; ++i)
-    {
         _controls[i]->update(elapsedTime);
+}
+
+void Container::updateState(State state)
+{
+    Control::updateState(state);
+
+    // Get scrollbar images and diminish clipping bounds to make room for scrollbars.
+    if ((_scroll & SCROLL_HORIZONTAL) == SCROLL_HORIZONTAL)
+    {
+        _scrollBarLeftCap = getImage("scrollBarLeftCap", state);
+        _scrollBarHorizontal = getImage("horizontalScrollBar", state);
+        _scrollBarRightCap = getImage("scrollBarRightCap", state);
+    }
+
+    if ((_scroll & SCROLL_VERTICAL) == SCROLL_VERTICAL)
+    {
+        _scrollBarTopCap = getImage("scrollBarTopCap", state);
+        _scrollBarVertical = getImage("verticalScrollBar", state);
+        _scrollBarBottomCap = getImage("scrollBarBottomCap", state);
     }
 }
 
-void Container::updateBounds(const Vector2& offset)
+bool Container::updateBounds(const Vector2& offset)
 {
-    if ((_dirtyBits & DIRTY_BOUNDS) == 0)
+    // Update our child bounds first
+    updateChildBounds();
+
+    /*if ((_dirtyBits & DIRTY_BOUNDS) == 0)
     {
-        // We're not dirty, but our children might be.
+        // We're not dirty, but our children might be
         updateChildBounds();
-        return;
-    }
 
-    Rectangle oldAbsoluteBounds(_absoluteBounds);
+        // If we're still not dirty after updating our child bounds, we have nothing to do
+        if ((_dirtyBits & DIRTY_BOUNDS) == 0)
+            return false;
+    }*/
 
-    Control::updateBounds(offset);
+    bool changed = Control::updateBounds(offset);
 
     Control::State state = getState();
 
     // Get scrollbar images and diminish clipping bounds to make room for scrollbars.
     if ((_scroll & SCROLL_HORIZONTAL) == SCROLL_HORIZONTAL)
     {
-        _scrollBarLeftCap = getImage("scrollBarLeftCap", state);
-        _scrollBarHorizontal = getImage("horizontalScrollBar", state);
-        _scrollBarRightCap = getImage("scrollBarRightCap", state);
-
         GP_ASSERT(_scrollBarLeftCap && _scrollBarHorizontal && _scrollBarRightCap);
-
         _viewportBounds.height -= _scrollBarHorizontal->getRegion().height;
         _viewportClipBounds.height -= _scrollBarHorizontal->getRegion().height;
     }
 
     if ((_scroll & SCROLL_VERTICAL) == SCROLL_VERTICAL)
     {
-        _scrollBarTopCap = getImage("scrollBarTopCap", state);
-        _scrollBarVertical = getImage("verticalScrollBar", state);
-        _scrollBarBottomCap = getImage("scrollBarBottomCap", state);
-
         GP_ASSERT(_scrollBarTopCap && _scrollBarVertical && _scrollBarBottomCap);
-
         _viewportBounds.width -= _scrollBarVertical->getRegion().width;
         _viewportClipBounds.width -= _scrollBarVertical->getRegion().width;
     }
@@ -559,9 +582,12 @@ void Container::updateBounds(const Vector2& offset)
     _layout->update(this);
 
     // Update bounds of our children
-    updateChildBounds();
+    //updateChildBounds();
+
+    changed = changed || (_dirtyBits & DIRTY_BOUNDS);
 
     // Handle automatically sizing based on our children
+    Rectangle oldBounds(_bounds);
     if (_autoSize != AUTO_SIZE_NONE)
     {
         if (_autoSize & AUTO_SIZE_WIDTH)
@@ -600,13 +626,29 @@ void Container::updateBounds(const Vector2& offset)
             setHeightInternal(height);
         }
     }
+    changed = changed || (_bounds != oldBounds);
 
     // If our bounds have changed, dirty our entire child hierarchy since at a minimum their absolute bounds
     // will need to be recomputed to be properly offset from our new bounds.
-    if (_absoluteBounds != oldAbsoluteBounds)
+    if (changed)
     {
-        setDirty(DIRTY_BOUNDS);
+        setChildrenDirty(DIRTY_BOUNDS, true);
     }
+    /*if ((_bounds != oldBounds) || (_dirtyBits & DIRTY_BOUNDS))
+    {
+        //setDirty(DIRTY_BOUNDS);
+        setChildrenDirty(DIRTY_BOUNDS, true);
+
+        if (!_inRelayout)
+        {
+            // Perform a second pass layout to update ourself based on our children and our children based on ourself
+            _inRelayout = true;
+            //updateBounds(offset);
+            _inRelayout = false;
+        }
+    }*/
+
+    return changed;
 }
 
 void Container::updateChildBounds()
@@ -616,18 +658,19 @@ void Container::updateChildBounds()
         Control* ctrl = _controls[i];
         GP_ASSERT(ctrl);
 
-        if (ctrl->isVisible() && (ctrl->isContainer() || ctrl->_dirtyBits & DIRTY_BOUNDS))
+        if (ctrl->isVisible() && (ctrl->isContainer() || (ctrl->_dirtyBits & DIRTY_BOUNDS)))
         {
-            Rectangle oldBounds(ctrl->_absoluteBounds);
+            //Rectangle oldBounds(ctrl->_absoluteBounds);
 
-            ctrl->updateBounds(_scrollPosition);
+            bool changed = ctrl->updateBounds(_scrollPosition);
 
             // If the child bounds have changed, dirty our bounds and all of our
             // parent bounds so that our layout and/or bounds are recomputed.
-            if (ctrl->_absoluteBounds != oldBounds)
+            //if (ctrl->_absoluteBounds != oldBounds)
+            if (changed)
             {
                 Control* parent = this;
-                while (parent)
+                while (parent && parent->_autoSize != AUTO_SIZE_NONE)
                 {
                     parent->setDirty(DIRTY_BOUNDS);
                     parent = parent->_parent;
@@ -1115,6 +1158,7 @@ void Container::updateScroll()
         }
 
         setDirty(DIRTY_BOUNDS);
+        setChildrenDirty(DIRTY_BOUNDS, true);
     }
 
     // Stop scrolling when the far edge is reached.
@@ -1183,7 +1227,8 @@ bool Container::touchEventScroll(Touch::TouchEvent evt, int x, int y, unsigned i
     case Touch::TOUCH_PRESS:
         if (_contactIndex == INVALID_CONTACT_INDEX)
         {
-            _contactIndex = (int) contactIndex;
+            bool dirty = !_scrollingVelocity.isZero();
+            _contactIndex = (int)contactIndex;
             _scrollingLastX = _scrollingFirstX = _scrollingVeryFirstX = x;
             _scrollingLastY = _scrollingFirstY = _scrollingVeryFirstY = y;
             _scrollingVelocity.set(0, 0);
@@ -1196,7 +1241,8 @@ bool Container::touchEventScroll(Touch::TouchEvent evt, int x, int y, unsigned i
                 _scrollBarOpacityClip = NULL;
             }
             _scrollBarOpacity = 1.0f;
-            setDirty(DIRTY_BOUNDS);
+            if (dirty)
+                setDirty(DIRTY_BOUNDS);
             return false;
         }
         break;
@@ -1260,6 +1306,7 @@ bool Container::touchEventScroll(Touch::TouchEvent evt, int x, int y, unsigned i
 
             _scrollingLastTime = gameTime;
             setDirty(DIRTY_BOUNDS);
+            setChildrenDirty(DIRTY_BOUNDS, true);
             return false;
         }
         break;
@@ -1326,6 +1373,7 @@ bool Container::mouseEventScroll(Mouse::MouseEvent evt, int x, int y, int wheelD
     {
         case Mouse::MOUSE_PRESS_LEFT_BUTTON:
         {
+            bool dirty = false;
             if (_scrollBarVertical)
             {
                 float vWidth = _scrollBarVertical->getRegion().width;
@@ -1344,10 +1392,12 @@ bool Container::mouseEventScroll(Mouse::MouseEvent evt, int x, int y, int wheelD
                     if (y < vBounds.y)
                     {
                         _scrollPosition.y += _totalHeight / 5.0f;
+                        dirty = true;
                     }
                     else if (y > vBounds.bottom())
                     {
                         _scrollPosition.y -= _totalHeight / 5.0f;
+                        dirty = true;
                     }
                     else
                     {
@@ -1373,10 +1423,12 @@ bool Container::mouseEventScroll(Mouse::MouseEvent evt, int x, int y, int wheelD
                     if (x < hBounds.x)
                     {
                         _scrollPosition.x += _totalWidth / 5.0f;
+                        dirty = true;
                     }
                     else if (x > hBounds.x + hBounds.width)
                     {
                         _scrollPosition.x -= _totalWidth / 5.0f;
+                        dirty = true;
                     }
                     else
                     {
@@ -1385,6 +1437,9 @@ bool Container::mouseEventScroll(Mouse::MouseEvent evt, int x, int y, int wheelD
                 }
             }
 
+            if (dirty)
+                setChildrenDirty(DIRTY_BOUNDS, true);
+
             return touchEventScroll(Touch::TOUCH_PRESS, x, y, 0);
         }
 

+ 15 - 1
gameplay/src/Container.h

@@ -309,16 +309,29 @@ protected:
      */
     void update(float elapsedTime);
 
+    /**
+     * @see Control::updateState
+     */
+    void updateState(State state);
+
     /**
      * @see Control::updateBounds
      */
-    void updateBounds(const Vector2& offset);
+    bool updateBounds(const Vector2& offset);
 
     /**
      * Updates the bounds for this container's child controls.
      */
     void updateChildBounds();
 
+    /**
+     * Sets the specified dirty bits for all children within this container.
+     *
+     * @param bits The bits to set.
+     * @param recursive If true, set the bits recursively on all children and their children.
+     */
+    void setChildrenDirty(int bits, bool recursive);
+
     /**
      * Gets a Layout::Type enum from a matching string.
      *
@@ -563,6 +576,7 @@ private:
     bool _contactIndices[MAX_CONTACT_INDICES];
     bool _initializedWithScroll;
     bool _scrollWheelRequiresFocus;
+    bool _inRelayout;
 };
 
 }

+ 78 - 62
gameplay/src/Control.cpp

@@ -46,7 +46,7 @@ static bool parseCoordPair(const char* s, float* v1, float* v2, bool* v1Percenta
 }
 
 Control::Control()
-    : _id(""), _enabled(true), _boundsBits(0), _dirtyBits(DIRTY_BOUNDS), _consumeInputEvents(true), _alignment(ALIGN_TOP_LEFT),
+    : _id(""), _enabled(true), _boundsBits(0), _dirtyBits(DIRTY_BOUNDS | DIRTY_STATE), _consumeInputEvents(true), _alignment(ALIGN_TOP_LEFT),
     _autoSize(AUTO_SIZE_BOTH), _style(NULL), _listeners(NULL), _visible(true), _zIndex(-1),
     _contactIndex(INVALID_CONTACT_INDEX), _focusIndex(-1), _canFocus(false), _parent(NULL), _styleOverridden(false), _skin(NULL)
 {
@@ -500,10 +500,10 @@ void Control::setVisible(bool visible)
     {
         _visible = visible;
 
-        if (_visible)
-            setDirty(DIRTY_BOUNDS);
-        else
+        if (!_visible)
             Form::controlDisabled(this);
+
+        setDirty(DIRTY_BOUNDS);
     }
 }
 
@@ -560,10 +560,6 @@ void Control::setOpacity(float opacity, unsigned char states)
         if (overlays[i])
             overlays[i]->setOpacity(opacity);
     }
-
-    // Opacity is currently pre-multiplied in updateBounds, so we need to 
-    // dirty our bounds when it changes.
-    setDirty(DIRTY_BOUNDS);
 }
 
 float Control::getOpacity(State state) const
@@ -582,9 +578,7 @@ void Control::setEnabled(bool enabled)
         if (!_enabled)
             Form::controlDisabled(this);
 
-        // The enabled flag modifies control state which can be themed, so we need to assume
-        // that the bounds may change as a result of this.
-        setDirty(DIRTY_BOUNDS);
+        setDirty(DIRTY_STATE);
     }
 }
 
@@ -1120,18 +1114,6 @@ void Control::controlEvent(Control::Listener::EventType evt)
 void Control::setDirty(int bits)
 {
     _dirtyBits |= bits;
-
-    if (bits & DIRTY_BOUNDS)
-    {
-        // When a container's bounds are dirty, all children controls also need
-        // to be dirtied so that their absolute bounds are updated correctly.
-        if (isContainer())
-        {
-            Container* container = static_cast<Container*>(this);
-            for (unsigned int i = 0, count = container->getControlCount(); i < count; ++i)
-                container->getControl(i)->setDirty(DIRTY_BOUNDS);
-        }
-    }
 }
 
 bool Control::isDirty(int bit) const
@@ -1141,11 +1123,40 @@ bool Control::isDirty(int bit) const
 
 void Control::update(float elapsedTime)
 {
+    State state = getState();
+
+    // Update visual state if it's dirty
+    if (_dirtyBits & DIRTY_STATE)
+        updateState(getState());
+
+    // Since opacity is pre-multiplied, we compute it every frame so that we don't need to
+    // dirty the entire hierarchy any time a state changes (which could affect opacity).
+    _opacity = getOpacity(state);
+    if (_parent)
+        _opacity *= _parent->_opacity;
 }
 
-void Control::updateBounds(const Vector2& offset)
+void Control::updateState(State state)
 {
-    // Clear our dirty bounds bit
+    // Clear dirty state bit
+    _dirtyBits &= ~DIRTY_STATE;
+
+    // Cache themed attributes for performance.
+    _skin = getSkin(state);
+}
+
+bool Control::updateBounds(const Vector2& offset)
+{
+    // If our state is currently dirty, update it here so that any rendering state objects needed
+    // for bounds computation are accessible.
+    State state = getState();
+    if (_dirtyBits & DIRTY_STATE)
+    {
+        updateState(state);
+        _dirtyBits &= ~DIRTY_STATE;
+    }
+
+    // Clear dirty bounds bit
     _dirtyBits &= ~DIRTY_BOUNDS;
 
     Game* game = Game::getInstance();
@@ -1153,17 +1164,18 @@ void Control::updateBounds(const Vector2& offset)
     const Rectangle parentAbsoluteBounds = _parent ? _parent->_viewportBounds : Rectangle(0, 0, game->getViewport().width, game->getViewport().height);
     const Rectangle parentAbsoluteClip = _parent ? _parent->_viewportClipBounds : parentAbsoluteBounds;
 
-    // Compute content area padding values
-    const Theme::Border& border = getBorder(getState());
-    const Theme::Padding& padding = getPadding();
-    float lpadding = border.left + padding.left;
-    float rpadding = border.right + padding.right;
-    float tpadding = border.top + padding.top;
-    float bpadding = border.bottom + padding.bottom;
-    float hpadding = lpadding + rpadding;
-    float vpadding = tpadding + bpadding;
+    // Calculate local unclipped bounds.
+    _bounds.set(_relativeBounds);
+    if (isXPercentage())
+        _bounds.x *= parentAbsoluteBounds.width;
+    if (isYPercentage())
+        _bounds.y *= parentAbsoluteBounds.height;
+    if (isWidthPercentage())
+        _bounds.width *= parentAbsoluteBounds.width;
+    if (isHeightPercentage())
+        _bounds.height *= parentAbsoluteBounds.height;
 
-    // Apply control alignment first
+    // Apply control alignment
     if (_alignment != Control::ALIGN_TOP_LEFT)
     {
         const Theme::Margin& margin = _style->getMargin();
@@ -1188,52 +1200,58 @@ void Control::updateBounds(const Vector2& offset)
         // Vertical alignment
         if ((_alignment & Control::ALIGN_BOTTOM) == Control::ALIGN_BOTTOM)
         {
-            setYInternal(clipHeight - _bounds.height - margin.bottom);
+            _bounds.y = clipHeight - _bounds.height - margin.bottom;
         }
         else if ((_alignment & Control::ALIGN_VCENTER) == Control::ALIGN_VCENTER)
         {
-            setYInternal(clipHeight * 0.5f - _bounds.height * 0.5f);
+            _bounds.y = clipHeight * 0.5f - _bounds.height * 0.5f;
         }
         else if ((_alignment & Control::ALIGN_TOP) == Control::ALIGN_TOP)
         {
-            setYInternal(margin.top);
+            _bounds.y = margin.top;
         }
 
         // Horizontal alignment
         if ((_alignment & Control::ALIGN_RIGHT) == Control::ALIGN_RIGHT)
         {
-            setXInternal(clipWidth - _bounds.width - margin.right);
+            _bounds.x = clipWidth - _bounds.width - margin.right;
         }
         else if ((_alignment & Control::ALIGN_HCENTER) == Control::ALIGN_HCENTER)
         {
-            setXInternal(clipWidth * 0.5f - _bounds.width * 0.5f);
+            _bounds.x = clipWidth * 0.5f - _bounds.width * 0.5f;
         }
         else if ((_alignment & Control::ALIGN_LEFT) == Control::ALIGN_LEFT)
         {
-            setXInternal(margin.left);
+            _bounds.x = margin.left;
         }
     }
 
-    // Calculate local unclipped bounds.
-    _bounds.set(_relativeBounds);
-    if (isXPercentage())
-        _bounds.x *= parentAbsoluteBounds.width;
-    if (isYPercentage())
-        _bounds.y *= parentAbsoluteBounds.height;
-    if (isWidthPercentage())
-        _bounds.width *= parentAbsoluteBounds.width;
-    if (isHeightPercentage())
-        _bounds.height *= parentAbsoluteBounds.height;
+    // Compute content area padding values
+    const Theme::Border& border = getBorder(state);
+    const Theme::Padding& padding = getPadding();
+    float lpadding = border.left + padding.left;
+    float rpadding = border.right + padding.right;
+    float tpadding = border.top + padding.top;
+    float bpadding = border.bottom + padding.bottom;
+    float hpadding = lpadding + rpadding;
+    float vpadding = tpadding + bpadding;
+
+    Rectangle temp;
+    bool changed = false;
 
     // Calculate absolute unclipped bounds
-    _absoluteBounds.set(
+    temp.set(
         parentAbsoluteBounds.x + offset.x + _bounds.x,
         parentAbsoluteBounds.y + offset.y + _bounds.y,
         _bounds.width,
         _bounds.height);
+    changed = temp != _absoluteBounds;
+    _absoluteBounds = temp;
 
     // Calculate absolute clipped bounds
-    Rectangle::intersect(_absoluteBounds, parentAbsoluteClip, &_absoluteClipBounds);
+    Rectangle::intersect(_absoluteBounds, parentAbsoluteClip, &temp);
+    changed = changed || (temp != _absoluteClipBounds);
+    _absoluteClipBounds = temp;
 
     // Calculate the local clipped bounds
     _clipBounds.set(
@@ -1244,22 +1262,20 @@ void Control::updateBounds(const Vector2& offset)
         );
 
     // Calculate the absolute unclipped viewport bounds (content area, which does not include border and padding)
-    _viewportBounds.set(
+    temp.set(
         _absoluteBounds.x + lpadding,
         _absoluteBounds.y + tpadding,
         _absoluteBounds.width - hpadding,
         _absoluteBounds.height - vpadding);
+    changed = changed || (temp != _viewportBounds);
+    _viewportBounds = temp;
 
     // Calculate the absolute clipped viewport bounds
-    Rectangle::intersect(_viewportBounds, parentAbsoluteClip, &_viewportClipBounds);
-
-    // Cache themed attributes for performance.
-    _skin = getSkin(getState());
+    Rectangle::intersect(_viewportBounds, parentAbsoluteClip, &temp);
+    changed = changed || (temp != _viewportClipBounds);
+    _viewportClipBounds = temp;
 
-    // Current opacity should be multiplied by that of the parent container.
-    _opacity = getOpacity(getState());
-    if (_parent)
-        _opacity *= _parent->_opacity;
+    return true;
 }
 
 void Control::startBatch(Form* form, SpriteBatch* batch)

+ 19 - 1
gameplay/src/Control.h

@@ -980,6 +980,11 @@ protected:
      */
     static const int DIRTY_BOUNDS = 1;
 
+    /**
+     * Indicates that the state of the control is dirty.
+     */
+    static const int DIRTY_STATE = 2;
+
     /**
      * Constructor.
      */
@@ -1110,14 +1115,27 @@ protected:
      */
     virtual void update(float elapsedTime);
 
+    /**
+     * Called when the state of the control has been updated.
+     *
+     * When the state of a control is updated (for example, a control becomes active, focused,
+     * hovered, disabled, etc), this method is called to allow implementations to update
+     * internal data related to control state.
+     *
+     * @param The new control state.
+     */
+    virtual void updateState(State state);
+
     /**
      * Updates the bounds for this control and its children.
      *
      * Child controls that need to customize their bounds calculation should override this method.
      *
      * @param offset Positioning offset to add to the control's position (most often used for scrolling).
+     *
+     * @return True if the bounds were updated, or false if there was no change.
      */
-    virtual void updateBounds(const Vector2& offset);
+    virtual bool updateBounds(const Vector2& offset);
 
     /**
      * Indicates that a control will begin drawing into the specified batch.

+ 25 - 18
gameplay/src/Form.cpp

@@ -191,13 +191,20 @@ void Form::update(float elapsedTime)
     updateBounds(Vector2::zero());
 }
 
-void Form::updateBounds(const Vector2& offset)
+bool Form::updateBounds(const Vector2& offset)
 {
-    // Two pass bounds update:
-    // 1. First pass computes child/leaf control sizes.
-    // 2. Second pass fits parent sizes to that of the children (and does relative sizing of children)
-    Container::updateBounds(offset);
-    Container::updateBounds(offset);
+    // Do a two-pass bounds update:
+    //  1. First pass updates leaf controls
+    //  2. Second pass updates parent controls that depend on child sizes
+    // Note: We could have updateBounds return a boolean indicating whether anything
+    // was dirtied as a result of the update (so we can optionally skip the second pass).
+    if (Container::updateBounds(offset))
+    {
+        Container::updateBounds(offset);
+        return true;
+    }
+
+    return false;
 }
 
 void Form::startBatch(SpriteBatch* batch)
@@ -406,11 +413,11 @@ Control* Form::handlePointerPressRelease(int* x, int* y, bool pressed)
             if (_activeControl != ctrl || _activeControlState != Control::ACTIVE)
             {
                 if (_activeControl)
-                    _activeControl->setDirty(Control::DIRTY_BOUNDS);
+                    _activeControl->setDirty(Control::DIRTY_STATE);
 
                 _activeControl = ctrl;
                 _activeControlState = Control::ACTIVE;
-                _activeControl->setDirty(Control::DIRTY_BOUNDS);
+                _activeControl->setDirty(Control::DIRTY_STATE);
             }
 
             ctrl->notifyListeners(Control::Listener::PRESS);
@@ -431,7 +438,7 @@ Control* Form::handlePointerPressRelease(int* x, int* y, bool pressed)
             screenToForm(ctrl, &newX, &newY);
 
             // No longer any active control
-            _activeControl->setDirty(Control::DIRTY_BOUNDS);
+            _activeControl->setDirty(Control::DIRTY_STATE);
             _activeControl = NULL;
             _activeControlState = Control::NORMAL;
         }
@@ -446,18 +453,18 @@ Control* Form::handlePointerPressRelease(int* x, int* y, bool pressed)
                 if (_activeControl != ctrl || _activeControlState != Control::HOVER)
                 {
                     if (_activeControl)
-                        _activeControl->setDirty(Control::DIRTY_BOUNDS);
+                        _activeControl->setDirty(Control::DIRTY_STATE);
 
                     _activeControl = ctrl;
                     _activeControlState = Control::HOVER;
-                    _activeControl->setDirty(Control::DIRTY_BOUNDS);
+                    _activeControl->setDirty(Control::DIRTY_STATE);
                 }
             }
             else
             {
                 // No longer any active control
                 if (_activeControl)
-                    _activeControl->setDirty(Control::DIRTY_BOUNDS);
+                    _activeControl->setDirty(Control::DIRTY_STATE);
 
                 _activeControl = NULL;
                 _activeControlState = Control::NORMAL;
@@ -509,18 +516,18 @@ Control* Form::handlePointerMove(int* x, int* y)
             if (_activeControl != ctrl || _activeControlState != Control::HOVER)
             {
                 if (_activeControl)
-                    _activeControl->setDirty(Control::DIRTY_BOUNDS);
+                    _activeControl->setDirty(Control::DIRTY_STATE);
 
                 _activeControl = ctrl;
                 _activeControlState = Control::HOVER;
-                _activeControl->setDirty(Control::DIRTY_BOUNDS);
+                _activeControl->setDirty(Control::DIRTY_STATE);
             }
         }
         else
         {
             // No active/hover control
             if (_activeControl)
-                _activeControl->setDirty(Control::DIRTY_BOUNDS);
+                _activeControl->setDirty(Control::DIRTY_STATE);
 
             _activeControl = NULL;
             _activeControlState = Control::NORMAL;
@@ -922,7 +929,7 @@ void Form::resizeEventInternal(unsigned int width, unsigned int height)
         if (form)
         {
             // Dirty the form
-            form->setDirty(Control::DIRTY_BOUNDS);
+            form->setDirty(Control::DIRTY_STATE);
         }
     }
 }
@@ -997,14 +1004,14 @@ void Form::setFocusControl(Control* control)
     // Deactivate the old focus control
     if (oldFocus)
     {
-        oldFocus->setDirty(Control::DIRTY_BOUNDS);
+        oldFocus->setDirty(Control::DIRTY_STATE);
         oldFocus->notifyListeners(Control::Listener::FOCUS_LOST);
     }
 
     // Activate the new focus control
     if (_focusControl)
     {
-        _focusControl->setDirty(Control::DIRTY_BOUNDS);
+        _focusControl->setDirty(Control::DIRTY_STATE);
         _focusControl->notifyListeners(Control::Listener::FOCUS_GAINED);
 
         // Set the activeControl property of the control's parent container

+ 3 - 3
gameplay/src/Form.h

@@ -107,9 +107,9 @@ public:
     void update(float elapsedTime);
 
     /**
-    * @see Control::updateBounds
-    */
-    void updateBounds(const Vector2& offset);
+     * @see Control::updtaeBounds
+     */
+    bool updateBounds(const Vector2& offset);
 
     /**
      * Draws this form.

+ 8 - 2
gameplay/src/ImageControl.cpp

@@ -138,9 +138,11 @@ unsigned int ImageControl::drawImages(Form* form, const Rectangle& clip)
     return 1;
 }
 
-void ImageControl::updateBounds(const Vector2& offset)
+bool ImageControl::updateBounds(const Vector2& offset)
 {
-    Control::updateBounds(offset);
+    bool changed = Control::updateBounds(offset);
+
+    Rectangle oldBounds(_bounds);
 
     if (_batch)
     {
@@ -154,6 +156,10 @@ void ImageControl::updateBounds(const Vector2& offset)
             setHeightInternal(_batch->getSampler()->getTexture()->getWidth());
         }
     }
+
+    changed = changed || (_bounds != oldBounds);
+
+    return changed;
 }
 
 }

+ 1 - 1
gameplay/src/ImageControl.h

@@ -124,7 +124,7 @@ protected:
     /**
      * @see Control::updateBounds
      */
-    void updateBounds(const Vector2& offset);
+    bool updateBounds(const Vector2& offset);
 
 private:
 

+ 32 - 13
gameplay/src/Label.cpp

@@ -57,11 +57,9 @@ void Label::addListener(Control::Listener* listener, int eventFlags)
     
 void Label::setText(const char* text)
 {
-    assert(text);
-
-    if (strcmp(text, _text.c_str()) != 0)
+    if ((text == NULL && _text.length() > 0) || strcmp(text, _text.c_str()) != 0)
     {
-        _text = text;
+        _text = text ? text : "";
         if (_autoSize != AUTO_SIZE_NONE)
             setDirty(DIRTY_BOUNDS);
     }
@@ -72,22 +70,39 @@ const char* Label::getText()
     return _text.c_str();
 }
 
-void Label::updateBounds(const Vector2& offset)
+void Label::update(float elapsedTime)
 {
-    Control::updateBounds(offset);
+    Control::update(elapsedTime);
 
-    _textBounds.set((int)_viewportBounds.x, (int)_viewportBounds.y, _viewportBounds.width, _viewportBounds.height);
+    // Update text opacity each frame since opacity is updated in Control::update.
+    _textColor = getTextColor(getState());
+    _textColor.w *= _opacity;
+}
+
+void Label::updateState(State state)
+{
+    Control::updateState(state);
 
-    Control::State state = getState();
     _font = getFont(state);
-    _textColor = getTextColor(state);
-    _textColor.w *= _opacity;
+}
 
-    Font* font = getFont(state);
-    if (_autoSize != AUTO_SIZE_NONE && font)
+bool Label::updateBounds(const Vector2& offset)
+{
+    bool changed = Control::updateBounds(offset);
+
+    // Measure bounds based only on normal state so that bounds updates are not always required on state changes.
+    // This is a trade-off for functionality vs performance, but changing the size of UI controls on hover/focus/etc
+    // is a pretty bad practice so we'll prioritize performance here.
+    Control::State state = NORMAL;
+
+    _textBounds.set((int)_viewportBounds.x, (int)_viewportBounds.y, _viewportBounds.width, _viewportBounds.height);
+
+    Rectangle oldBounds(_bounds);
+
+    if (_autoSize != AUTO_SIZE_NONE && _font)
     {
         unsigned int w, h;
-        font->measureText(_text.c_str(), getFontSize(state), &w, &h);
+        _font->measureText(_text.c_str(), getFontSize(state), &w, &h);
         if (_autoSize & AUTO_SIZE_WIDTH)
         {
             setWidthInternal(w + getBorder(state).left + getBorder(state).right + getPadding().left + getPadding().right);
@@ -97,6 +112,10 @@ void Label::updateBounds(const Vector2& offset)
             setHeightInternal(h + getBorder(state).top + getBorder(state).bottom + getPadding().top + getPadding().bottom);
         }
     }
+
+    changed = changed || (_bounds != oldBounds);
+
+    return changed;
 }
 
 unsigned int Label::drawText(Form* form, const Rectangle& clip)

+ 11 - 1
gameplay/src/Label.h

@@ -91,10 +91,20 @@ protected:
      */
     void initialize(const char* typeName, Theme::Style* style, Properties* properties);
 
+    /**
+     * @see Control::update
+     */
+    void update(float elapsedTime);
+
+    /**
+     * @see Control::updateState
+     */
+    void updateState(State state);
+
     /**
      * @see Control::updateBounds
      */
-    void updateBounds(const Vector2& offset);
+    bool updateBounds(const Vector2& offset);
 
     /**
      * @see Control::drawText

+ 19 - 7
gameplay/src/RadioButton.cpp

@@ -73,7 +73,7 @@ void RadioButton::setSelected(bool selected)
     if (selected != _selected)
     {
         _selected = selected;
-        setDirty(DIRTY_BOUNDS);
+        setDirty(DIRTY_STATE);
         notifyListeners(Control::Listener::VALUE_CHANGED);
     }
 }
@@ -131,24 +131,34 @@ void RadioButton::controlEvent(Control::Listener::EventType evt)
     }
 }
 
-void RadioButton::updateBounds(const Vector2& offset)
+void RadioButton::updateState(State state)
 {
-    Label::updateBounds(offset);
+    Label::updateState(state);
+
+    _image = getImage(_selected ? "selected" : "unselected", state);
+}
+
+bool RadioButton::updateBounds(const Vector2& offset)
+{
+    bool changed = Label::updateBounds(offset);
     
-    Control::State state = getState();
+    // Compute bounds based on normal state only
+    Control::State state = NORMAL;
 
     Vector2 size;
     if (_selected)
     {
-        const Rectangle& selectedRegion = getImageRegion("selected", getState());
+        const Rectangle& selectedRegion = getImageRegion("selected", state);
         size.set(selectedRegion.width, selectedRegion.height);
     }
     else
     {
-        const Rectangle& unselectedRegion = getImageRegion("unselected", getState());
+        const Rectangle& unselectedRegion = getImageRegion("unselected", state);
         size.set(unselectedRegion.width, unselectedRegion.height);
     }
 
+    Rectangle oldBounds(_bounds);
+
     if (_autoSize & AUTO_SIZE_HEIGHT)
     {
         // Text-only width was already measured in Label::update - append image
@@ -163,9 +173,11 @@ void RadioButton::updateBounds(const Vector2& offset)
         setWidthInternal(_viewportBounds.height + 5 + _bounds.width);
     }
 
+    changed = changed || (_bounds != oldBounds);
+
     _textBounds.x += _viewportBounds.height + 5;
 
-    _image = getImage(_selected ? "selected" : "unselected", getState());
+    return changed;
 }
 
 unsigned int RadioButton::drawImages(Form* form, const Rectangle& clip)

+ 6 - 1
gameplay/src/RadioButton.h

@@ -117,10 +117,15 @@ protected:
      */
     void controlEvent(Control::Listener::EventType evt);
 
+    /**
+     * @see Control::updateState
+     */
+    void updateState(State state);
+
     /**
      * @see Control::updateBounds
      */
-    void updateBounds(const Vector2& offset);
+    bool updateBounds(const Vector2& offset);
 
     /**
      * @see Control::drawImages

+ 15 - 4
gameplay/src/Slider.cpp

@@ -330,16 +330,21 @@ void Slider::update(float elapsedTime)
     }
 }
 
-void Slider::updateBounds(const Vector2& offset)
+void Slider::updateState(State state)
 {
-    Label::updateBounds(offset);
-
-    Control::State state = getState();
+    Label::updateState(state);
 
     _minImage = getImage("minCap", state);
     _maxImage = getImage("maxCap", state);
     _markerImage = getImage("marker", state);
     _trackImage = getImage("track", state);
+}
+
+bool Slider::updateBounds(const Vector2& offset)
+{
+    bool changed = Label::updateBounds(offset);
+
+    Control::State state = getState();
 
     // Compute height of track (max of track, min/max and marker
     _trackHeight = _minImage->getRegion().height;
@@ -347,6 +352,8 @@ void Slider::updateBounds(const Vector2& offset)
     _trackHeight = std::max(_trackHeight, _markerImage->getRegion().height);
     _trackHeight = std::max(_trackHeight, _trackImage->getRegion().height);
 
+    Rectangle oldBounds(_bounds);
+
     if (_autoSize & AUTO_SIZE_HEIGHT)
     {
         float height = _bounds.height + _trackHeight;
@@ -354,6 +361,10 @@ void Slider::updateBounds(const Vector2& offset)
             height += getFontSize(state);
         setHeightInternal(height);
     }
+
+    changed = changed || (_bounds != oldBounds);
+
+    return changed;
 }
 
 unsigned int Slider::drawImages(Form* form, const Rectangle& clip)

+ 6 - 1
gameplay/src/Slider.h

@@ -237,10 +237,15 @@ protected:
      */
     void update(float elapsedTime);
 
+    /**
+     * @see Control::updateState
+     */
+    void updateState(State state);
+
     /**
      * @see Control::updateBounds
      */
-    void updateBounds(const Vector2& offset);
+    bool updateBounds(const Vector2& offset);
 
     /**
      * The minimum value for the Slider.

+ 2 - 3
gameplay/src/TextBox.cpp

@@ -306,11 +306,10 @@ void TextBox::controlEvent(Control::Listener::EventType evt)
     }
 }
 
-void TextBox::updateBounds(const Vector2& offset)
+void TextBox::updateState(State state)
 {
-    Label::updateBounds(offset);
+    Label::updateState(state);
 
-    Control::State state = getState();
     _fontSize = getFontSize(state);
     _caretImage = getImage("textCaret", state);
 }

+ 2 - 2
gameplay/src/TextBox.h

@@ -166,9 +166,9 @@ protected:
     void controlEvent(Control::Listener::EventType evt);
 
     /**
-     * @see Control::updateBounds
+     * @see Control::updateState
      */
-    void updateBounds(const Vector2& offset);
+    void updateState(State state);
 
     /**
      * @see Control::drawImages