Bladeren bron

All controls now take focus in the same way.
Tab key switches focus to next control in a container.
Fixed a memory leak in Form when using the empty style.
Removed unneccessary comments from SpriteBatch.
Fixed some crashes in TextBox.
Adding mouse event handler to Slider.

Adam Blake 13 jaren geleden
bovenliggende
commit
93c477d644

+ 4 - 0
gameplay/gameplay.vcxproj

@@ -35,6 +35,7 @@
     <ClCompile Include="src\Curve.cpp" />
     <ClCompile Include="src\DebugNew.cpp" />
     <ClCompile Include="src\DepthStencilTarget.cpp" />
+    <ClCompile Include="src\DropDownList.cpp" />
     <ClCompile Include="src\Effect.cpp" />
     <ClCompile Include="src\FileSystem.cpp" />
     <ClCompile Include="src\FlowLayout.cpp" />
@@ -52,6 +53,7 @@
     <ClCompile Include="src\Label.cpp" />
     <ClCompile Include="src\Layout.cpp" />
     <ClCompile Include="src\Light.cpp" />
+    <ClCompile Include="src\List.cpp" />
     <ClCompile Include="src\Material.cpp" />
     <ClCompile Include="src\MeshBatch.cpp" />
     <ClCompile Include="src\Pass.cpp" />
@@ -127,6 +129,7 @@
     <ClInclude Include="src\Curve.h" />
     <ClInclude Include="src\DebugNew.h" />
     <ClInclude Include="src\DepthStencilTarget.h" />
+    <ClInclude Include="src\DropDownList.h" />
     <ClInclude Include="src\Effect.h" />
     <ClInclude Include="src\FileSystem.h" />
     <ClInclude Include="src\FlowLayout.h" />
@@ -143,6 +146,7 @@
     <ClInclude Include="src\Label.h" />
     <ClInclude Include="src\Layout.h" />
     <ClInclude Include="src\Light.h" />
+    <ClInclude Include="src\List.h" />
     <ClInclude Include="src\Material.h" />
     <ClInclude Include="src\MathUtil.h" />
     <ClInclude Include="src\MeshBatch.h" />

+ 12 - 0
gameplay/gameplay.vcxproj.filters

@@ -279,6 +279,12 @@
     <ClCompile Include="src\Joystick.cpp">
       <Filter>src</Filter>
     </ClCompile>
+    <ClCompile Include="src\DropDownList.cpp">
+      <Filter>src</Filter>
+    </ClCompile>
+    <ClCompile Include="src\List.cpp">
+      <Filter>src</Filter>
+    </ClCompile>
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="src\Animation.h">
@@ -557,6 +563,12 @@
     <ClInclude Include="src\MathUtil.h">
       <Filter>src</Filter>
     </ClInclude>
+    <ClInclude Include="src\DropDownList.h">
+      <Filter>src</Filter>
+    </ClInclude>
+    <ClInclude Include="src\List.h">
+      <Filter>src</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <None Include="res\shaders\bumped-specular.vsh">

+ 38 - 3
gameplay/src/Button.cpp

@@ -12,6 +12,16 @@ Button::~Button()
 {
 }
 
+Button* Button::create(const char* id, Theme::Style* style)
+{
+    Button* button = new Button();
+
+    button->_id = id;
+    button->_style = style;
+
+    return button;
+}
+
 Button* Button::create(Theme::Style* style, Properties* properties)
 {
     Button* button = new Button();
@@ -30,14 +40,39 @@ bool Button::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contac
     switch (evt)
     {
     case Touch::TOUCH_PRESS:
-        setState(Control::ACTIVE);
+        if (x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width &&
+            y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height)
+        {
+            setState(Control::ACTIVE);
+            notifyListeners(Listener::PRESS);
+            return _consumeInputEvents;
+        }
+        else
+        {
+            setState(Control::NORMAL);
+        }
         break;
+
     case Touch::TOUCH_RELEASE:
-        setState(Control::NORMAL);
+        notifyListeners(Listener::RELEASE);
+        if (x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width &&
+            y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height)
+        {
+            setState(Control::FOCUS);
+            notifyListeners(Listener::CLICK);
+            return _consumeInputEvents;
+        }
+        else
+        {
+            setState(Control::NORMAL);
+        }
         break;
+
+    case Touch::TOUCH_MOVE:
+        return _consumeInputEvents;
     }
 
-    return Control::touchEvent(evt, x, y, contactIndex);
+    return false;
 }
 
 }

+ 12 - 0
gameplay/src/Button.h

@@ -33,6 +33,18 @@ class Button : public Label
 {
     friend class Container;
 
+public:
+
+    /**
+     * Create a new button control.
+     *
+     * @param id The control's ID.
+     * @param style The control's style.
+     *
+     * @return The new button.
+     */
+    static Button* create(const char* id, Theme::Style* style);
+
 protected:
 
     /**

+ 12 - 0
gameplay/src/CheckBox.cpp

@@ -19,6 +19,18 @@ CheckBox::~CheckBox()
 
 }
 
+CheckBox* CheckBox::create(const char* id, Theme::Style* style)
+{
+    GP_ASSERT(style);
+
+    CheckBox* checkBox = new CheckBox();
+    if (id)
+        checkBox->_id = id;
+    checkBox->setStyle(style);
+
+    return checkBox;
+}
+
 CheckBox* CheckBox::create(Theme::Style* style, Properties* properties)
 {
     GP_ASSERT(properties);

+ 13 - 3
gameplay/src/CheckBox.h

@@ -37,6 +37,16 @@ class CheckBox : public Button
 
 public:
 
+    /**
+     * Create a new check box control.
+     *
+     * @param id The control's ID.
+     * @param style The control's style.
+     *
+     * @return The new check box.
+     */
+    static CheckBox* create(const char* id, Theme::Style* style);
+
     /**
      * Gets whether this checkbox is checked.
      *
@@ -130,7 +140,7 @@ protected:
      * @param clip The container position this control is relative to.
      */
     void drawImages(SpriteBatch* spriteBatch, const Rectangle& clip);
-
+
     /**
      * Whether this checkbox is currently checked.
      */
@@ -140,11 +150,11 @@ protected:
      * The size to draw the checkbox icon, if different from its size in the texture.
      */
     Vector2 _imageSize;
-
+
     /**
      * The Theme::ThemeImage to display for the checkbox.
      */
-    Theme::ThemeImage* _image;
+    Theme::ThemeImage* _image;
 
 private:
 

+ 80 - 15
gameplay/src/Container.cpp

@@ -11,6 +11,7 @@
 #include "Slider.h"
 #include "TextBox.h"
 #include "Joystick.h"
+#include "List.h"
 #include "Game.h"
 
 namespace gameplay
@@ -31,7 +32,7 @@ Container::Container()
       _scrollingVelocity(Vector2::zero()), _scrollingFriction(1.0f),
       _scrollingRight(false), _scrollingDown(false),
       _scrollingMouseVertically(false), _scrollingMouseHorizontally(false),
-      _scrollBarOpacityClip(NULL), _zIndexDefault(0), _totalWidth(0), _totalHeight(0)
+      _scrollBarOpacityClip(NULL), _zIndexDefault(0), _focusIndexDefault(0), _focusIndexMax(0), _totalWidth(0), _totalHeight(0)
 {
 }
 
@@ -49,6 +50,17 @@ Container::~Container()
     SAFE_RELEASE(_layout);
 }
 
+Container* Container::create(const char* id, Theme::Style* style, Layout::Type layoutType)
+{
+    GP_ASSERT(style);
+
+    Container* container = Container::create(layoutType);
+    if (id)
+        container->_id = id;
+    container->_style = style;
+    return container;
+}
+
 Container* Container::create(Layout::Type type)
 {
     Layout* layout = NULL;
@@ -146,6 +158,10 @@ void Container::addControls(Theme* theme, Properties* properties)
         {
             control = Joystick::create(controlStyle, controlSpace);
         }
+        else if (controlName == "LIST")
+        {
+            control = List::create(controlStyle, controlSpace);
+        }
         else
         {
             GP_ERROR("Failed to create control; unrecognized control name '%s'.", controlName.c_str());
@@ -160,6 +176,15 @@ void Container::addControls(Theme* theme, Properties* properties)
             {
                 control->setZIndex(_zIndexDefault++);
             }
+
+            if (control->getFocusIndex() == -1)
+            {
+                control->setFocusIndex(_focusIndexDefault++);
+            }
+
+            int focusIndex = control->getFocusIndex();
+            if (focusIndex > _focusIndexMax)
+                _focusIndexMax = focusIndex;
         }
 
         // Get the next control.
@@ -395,7 +420,8 @@ void Container::draw(SpriteBatch* spriteBatch, const Rectangle& clip, bool needs
 
         spriteBatch->begin();
 
-        if (_scrollBarBounds.height > 0)
+        if (_scrollBarBounds.height > 0 &&
+            ((_scroll & SCROLL_VERTICAL) == SCROLL_VERTICAL))
         {
             const Rectangle& topRegion = _scrollBarTopCap->getRegion();
             const Theme::UVs& topUVs = _scrollBarTopCap->getUVs();
@@ -428,7 +454,8 @@ void Container::draw(SpriteBatch* spriteBatch, const Rectangle& clip, bool needs
             spriteBatch->draw(bounds.x, bounds.y, bounds.width, bounds.height, bottomUVs.u1, bottomUVs.v1, bottomUVs.u2, bottomUVs.v2, bottomColor, clipRegion);
         }
 
-        if (_scrollBarBounds.width > 0)
+        if (_scrollBarBounds.width > 0 &&
+            ((_scroll & SCROLL_HORIZONTAL) == SCROLL_HORIZONTAL))
         {
             const Rectangle& leftRegion = _scrollBarLeftCap->getRegion();
             const Theme::UVs& leftUVs = _scrollBarLeftCap->getUVs();
@@ -518,10 +545,33 @@ bool Container::keyEvent(Keyboard::KeyEvent evt, int key)
             continue;
         }
 
-        if (control->isContainer() || control->getState() == Control::FOCUS)
+        if (control->getState() == Control::FOCUS)
         {
             if (control->keyEvent(evt, key))
+            {
                 return _consumeInputEvents;
+            }
+            else if (evt == Keyboard::KEY_CHAR && key == Keyboard::KEY_TAB)
+            {
+                // Shift focus to next control.
+                int focusIndex = control->getFocusIndex() + 1; // Index to search for.
+                if (focusIndex > _focusIndexMax)
+                {
+                    focusIndex = 0;
+                }
+                control->setState(Control::NORMAL);
+
+                std::vector<Control*>::const_iterator itt;
+                for (itt = _controls.begin(); itt < _controls.end(); itt++)
+                {
+                    Control* nextControl = *itt;
+                    if (nextControl->getFocusIndex() == focusIndex)
+                    {
+                        nextControl->setState(Control::FOCUS);
+                        return _consumeInputEvents;
+                    }
+                }
+            }
         }
     }
 
@@ -873,6 +923,12 @@ bool Container::mouseEventScroll(Mouse::MouseEvent evt, int x, int y, int wheelD
 
         case Mouse::MOUSE_WHEEL:
             _scrollPosition.y += (_totalHeight / 10.0f) * wheelDelta;
+            if (_scrollBarOpacityClip && _scrollBarOpacityClip->isPlaying())
+            {
+                _scrollBarOpacityClip->stop();
+                _scrollBarOpacityClip = NULL;
+            }
+            _scrollBarOpacity = 1.0f;
             return _consumeInputEvents;
     }
 
@@ -917,7 +973,9 @@ bool Container::pointerEvent(bool mouse, char evt, int x, int y, int data)
             boundsY += offset->y;
         }
 
-        if (control->getState() != Control::NORMAL ||
+        Control::State currentState = control->getState();
+        if ((control->isContainer() && currentState == Control::FOCUS) ||
+            currentState != Control::NORMAL ||
             ((evt == Touch::TOUCH_PRESS ||
               evt == Mouse::MOUSE_PRESS_LEFT_BUTTON ||
               evt == Mouse::MOUSE_PRESS_MIDDLE_BUTTON ||
@@ -941,25 +999,32 @@ bool Container::pointerEvent(bool mouse, char evt, int x, int y, int data)
         return (_consumeInputEvents | eventConsumed);
     }
 
-    switch (evt)
-    {
-    case Touch::TOUCH_PRESS:
-        setState(Control::FOCUS);
-        break;
-    case Touch::TOUCH_RELEASE:
-        setState(Control::NORMAL);
-        break;
-    }
-
     if (!eventConsumed && _scroll != SCROLL_NONE)
     {
         if (mouse && mouseEventScroll((Mouse::MouseEvent)evt, x - xPos, y - yPos, data) ||
             (!mouse && touchEventScroll((Touch::TouchEvent)evt, x - xPos, y - yPos, (unsigned int)data)))
         {
             _dirty = true;
+            eventConsumed = true;
         }
     }
 
+    switch (evt)
+    {
+    case Touch::TOUCH_PRESS:
+        if (x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width &&
+            y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height)
+        {
+            setState(Control::FOCUS);
+        }
+        else
+        {
+            setState(Control::NORMAL);
+            return false;
+        }
+        break;
+    }
+
     return (_consumeInputEvents | eventConsumed);
 }
 

+ 13 - 1
gameplay/src/Container.h

@@ -45,7 +45,6 @@ namespace gameplay
  */
 class Container : public Control
 {
-    friend class DropDownList;
 
 public:
 
@@ -65,6 +64,17 @@ public:
         SCROLL_BOTH = SCROLL_HORIZONTAL | SCROLL_VERTICAL
     };
 
+    /**
+     * Create a new container.
+     *
+     * @param id The container's ID.
+     * @param style The container's style.
+     * @param layoutType The container's layout type.
+     *
+     * @return The new container.
+     */
+    static Container* create(const char* id, Theme::Style* style, Layout::Type layoutType = Layout::LAYOUT_ABSOLUTE);
+
     /**
      * Get this container's layout.
      *
@@ -458,6 +468,8 @@ private:
 
     AnimationClip* _scrollBarOpacityClip;
     int _zIndexDefault;
+    int _focusIndexDefault;
+    int _focusIndexMax;
 
     float _totalWidth;
     float _totalHeight;

+ 42 - 6
gameplay/src/Control.cpp

@@ -52,6 +52,15 @@ void Control::initialize(Theme::Style* style, Properties* properties)
         _zIndex = -1;
     }
 
+    if (properties->exists("focusIndex"))
+    {
+        _focusIndex = properties->getInt("focusIndex");
+    }
+    else
+    {
+        _focusIndex = -1;
+    }
+
     Vector2 position;
     Vector2 size;
     if (properties->exists("position"))
@@ -75,8 +84,6 @@ void Control::initialize(Theme::Style* style, Properties* properties)
     }
     setBounds(Rectangle(position.x, position.y, size.x, size.y));
 
-    _state = Control::getState(properties->getString("state"));
-
     const char* id = properties->getId();
     if (id)
         _id = id;
@@ -121,6 +128,11 @@ void Control::initialize(Theme::Style* style, Properties* properties)
     }
 }
 
+void initialize(const char* id, Theme::Style* style, const Vector2& position, const Vector2& size)
+{
+
+}
+
 const char* Control::getID() const
 {
     return _id.c_str();
@@ -631,6 +643,16 @@ void Control::setZIndex(int zIndex)
     }
 }
 
+int Control::getFocusIndex() const
+{
+    return _focusIndex;
+}
+
+void Control::setFocusIndex(int focusIndex)
+{
+    _focusIndex = focusIndex;
+}
+
 void Control::addListener(Control::Listener* listener, int eventFlags)
 {
     GP_ASSERT(listener);
@@ -691,17 +713,31 @@ bool Control::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int conta
     switch (evt)
     {
     case Touch::TOUCH_PRESS:
-        notifyListeners(Listener::PRESS);
-        return _consumeInputEvents;
+        // Controls that don't have an ACTIVE state go to the FOCUS state when pressed.
+        // (Other controls, such as buttons and sliders, become ACTIVE when pressed and go to the FOCUS state on release.)
+        // Labels are never any state other than NORMAL.
+        if (x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width &&
+            y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height)
+        {
+            notifyListeners(Listener::PRESS);
+            return _consumeInputEvents;
+        }
+        else
+        {
+            // If this control was in focus, it's not any more.
+            _state = NORMAL;
+        }
+        break;
             
     case Touch::TOUCH_RELEASE:
         // Always trigger Listener::RELEASE
         notifyListeners(Listener::RELEASE);
 
         // Only trigger Listener::CLICK if both PRESS and RELEASE took place within the control's bounds.
-        if (x > 0 && x <= _bounds.width &&
-            y > 0 && y <= _bounds.height)
+        if (x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width &&
+            y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height)
         {
+            // Leave this control in the FOCUS state.
             notifyListeners(Listener::CLICK);
         }
         return _consumeInputEvents;

+ 33 - 1
gameplay/src/Control.h

@@ -683,6 +683,20 @@ public:
      */
     void setZIndex(int zIndex);
 
+    /**
+     * Get this control's focus index.
+     *
+     * @return This control's focus index.
+     */
+    int getFocusIndex() const;
+
+    /**
+     * Set this control's focus index.
+     *
+     * @param focusIndex The new focus index.
+     */
+    void setFocusIndex(int focusIndex);
+
     /**
      * Add a listener to be notified of specific events affecting
      * this control.  Event types can be OR'ed together.
@@ -807,10 +821,23 @@ protected:
     virtual void draw(SpriteBatch* spriteBatch, const Rectangle& clip, bool needsClear, bool cleared, float targetHeight);
 
     /**
-     * Initialize properties common to STATE_ALL Controls.
+     * Initialize properties common to all Controls from a Properties object.
+     *
+     * @param style The style to apply to this control.
+     * @param properties The properties to set on this control.
      */
     virtual void initialize(Theme::Style* style, Properties* properties);
 
+    /**
+     * Initialize properties common to all Controls.
+     *
+     * @param id This control's ID.
+     * @param style The style to apply to this control.
+     * @param position This control's position.
+     * @param size This control's size.
+     */
+    //virtual void initialize(const char* id, Theme::Style* style, const Vector2& position, const Vector2& size);
+
     /**
      * Container and classes that extend it should implement this and return true.
      *
@@ -949,6 +976,11 @@ protected:
      */
     int _zIndex;
 
+    /**
+     * The focus order of the control.
+     */
+    int _focusIndex;
+
 private:
 
     /*

+ 51 - 9
gameplay/src/Form.cpp

@@ -39,6 +39,42 @@ Form::~Form()
     }
 }
 
+Form* Form::create(const char* id, Theme::Style* style, Layout::Type layoutType)
+{
+    GP_ASSERT(style);
+
+    Layout* layout;
+    switch (layoutType)
+    {
+    case Layout::LAYOUT_ABSOLUTE:
+        layout = AbsoluteLayout::create();
+        break;
+    case Layout::LAYOUT_FLOW:
+        layout = FlowLayout::create();
+        break;
+    case Layout::LAYOUT_VERTICAL:
+        layout = VerticalLayout::create();
+        break;
+    default:
+        GP_ERROR("Unsupported layout type '%d'.", layoutType);
+    }
+
+    Form* form = new Form();
+    if (id)
+        form->_id = id;
+    form->_style = style;
+    form->_layout = layout;
+    form->_theme = style->getTheme();
+
+    // Get default projection matrix.
+    Game* game = Game::getInstance();
+    Matrix::createOrthographicOffCenter(0, game->getWidth(), game->getHeight(), 0, 0, 1, &form->_defaultProjectionMatrix);
+
+    __forms.push_back(form);
+
+    return form;
+}
+
 Form* Form::create(const char* url)
 {
     // Load Form from .form file.
@@ -98,9 +134,7 @@ Form* Form::create(const char* url)
     }
     else
     {
-        Theme::Style::Overlay* overlay = Theme::Style::Overlay::create();
-        style = new Theme::Style(theme, "", 1.0f / theme->_texture->getWidth(), 1.0f / theme->_texture->getHeight(),
-            Theme::Margin::empty(), Theme::Border::empty(), overlay, overlay, overlay, overlay);
+        style = theme->getEmptyStyle();
     }
     form->initialize(style, formProperties);
 
@@ -158,6 +192,11 @@ Form* Form::getForm(const char* id)
     return NULL;
 }
 
+Theme* Form::getTheme() const
+{
+    return _theme;
+}
+
 void Form::setSize(float width, float height)
 {
     if (_autoWidth)
@@ -535,6 +574,7 @@ bool Form::touchEventInternal(Touch::TouchEvent evt, int x, int y, unsigned int
 {
     // Check for a collision with each Form in __forms.
     // Pass the event on.
+    bool eventConsumed = false;
     std::vector<Form*>::const_iterator it;
     for (it = __forms.begin(); it < __forms.end(); it++)
     {
@@ -556,7 +596,7 @@ bool Form::touchEventInternal(Touch::TouchEvent evt, int x, int y, unsigned int
                          point.y >= bounds.y &&
                          point.y <= bounds.y + bounds.height))
                     {
-                        return form->touchEvent(evt, point.x - bounds.x, bounds.height - point.y - bounds.y, contactIndex);
+                        eventConsumed |= form->touchEvent(evt, point.x - bounds.x, bounds.height - point.y - bounds.y, contactIndex);
                     }
                 }
             }
@@ -572,13 +612,13 @@ bool Form::touchEventInternal(Touch::TouchEvent evt, int x, int y, unsigned int
                         y <= bounds.y + bounds.height))
                 {
                     // Pass on the event's position relative to the form.
-                    return form->touchEvent(evt, x - bounds.x, y - bounds.y, contactIndex);
+                    eventConsumed |= form->touchEvent(evt, x - bounds.x, y - bounds.y, contactIndex);
                 }
             }
         }
     }
 
-    return false;
+    return eventConsumed;
 }
 
 bool Form::keyEventInternal(Keyboard::KeyEvent evt, int key)
@@ -600,6 +640,8 @@ bool Form::keyEventInternal(Keyboard::KeyEvent evt, int key)
 
 bool Form::mouseEventInternal(Mouse::MouseEvent evt, int x, int y, int wheelDelta)
 {
+    bool eventConsumed = false;
+
     std::vector<Form*>::const_iterator it;
     for (it = __forms.begin(); it < __forms.end(); it++)
     {
@@ -624,7 +666,7 @@ bool Form::mouseEventInternal(Mouse::MouseEvent evt, int x, int y, int wheelDelt
                          point.y >= bounds.y &&
                          point.y <= bounds.y + bounds.height))
                     {
-                        return form->mouseEvent(evt, point.x - bounds.x, bounds.height - point.y - bounds.y, wheelDelta);
+                        eventConsumed |= form->mouseEvent(evt, point.x - bounds.x, bounds.height - point.y - bounds.y, wheelDelta);
                     }
                 }
             }
@@ -643,13 +685,13 @@ bool Form::mouseEventInternal(Mouse::MouseEvent evt, int x, int y, int wheelDelt
                         y <= bounds.y + bounds.height))
                 {
                     // Pass on the event's position relative to the form.
-                    return form->mouseEvent(evt, x - bounds.x, y - bounds.y, wheelDelta);
+                    eventConsumed |= form->mouseEvent(evt, x - bounds.x, y - bounds.y, wheelDelta);
                 }
             }
         }
     }
 
-    return false;
+    return eventConsumed;
 }
 
 bool Form::projectPoint(int x, int y, Vector3* point)

+ 14 - 1
gameplay/src/Form.h

@@ -55,10 +55,21 @@ public:
      * where the URL is of the format "<file-path>.<extension>#<namespace-id>/<namespace-id>/.../<namespace-id>"
      * (and "#<namespace-id>/<namespace-id>/.../<namespace-id>" is optional). 
      * 
-     * @param url The URL pointing to the Properties object defining the animation data. 
+     * @param url The URL pointing to the Properties object defining the form. 
      */
     static Form* create(const char* url);
 
+    /**
+     * Create a new Form.
+     *
+     * @param id The Form's ID.
+     * @param style The Form's style.
+     * @param layoutType The form's layout type.
+     *
+     * @return The new Form.
+     */
+    static Form* create(const char* id, Theme::Style* style, Layout::Type layoutType = Layout::LAYOUT_ABSOLUTE);
+
     /**
      * Get a form from its ID.
      *
@@ -68,6 +79,8 @@ public:
      */
     static Form* getForm(const char* id);
 
+    Theme* getTheme() const;
+
     /**
      * Set the desired size of this form.
      *

+ 12 - 0
gameplay/src/Joystick.cpp

@@ -18,6 +18,18 @@ Joystick::~Joystick()
 {
 }
 
+Joystick* Joystick::create(const char* id, Theme::Style* style)
+{
+    GP_ASSERT(style);
+
+    Joystick* joystick = new Joystick();
+    if (id)
+        joystick->_id = id;
+    joystick->setStyle(style);
+
+    return joystick;
+}
+
 Joystick* Joystick::create(Theme::Style* style, Properties* properties)
 {
     Joystick* joystick = new Joystick();

+ 10 - 0
gameplay/src/Joystick.h

@@ -14,6 +14,16 @@ class Joystick : public Control
     friend class Container;
 
 public:
+
+    /**
+     * Create a new joystick control.
+     *
+     * @param id The control's ID.
+     * @param style The control's style.
+     *
+     * @return The new joystick.
+     */
+    static Joystick* create(const char* id, Theme::Style* style);
     
     /**
      * Add a listener to be notified of specific events affecting

+ 15 - 0
gameplay/src/Label.cpp

@@ -16,11 +16,26 @@ Label::~Label()
 {
 }
 
+Label* Label::create(const char* id, Theme::Style* style)
+{
+    GP_ASSERT(style);
+
+    Label* label = new Label();
+    if (id)
+        label->_id = id;
+    label->setStyle(style);
+
+    return label;
+}
+
 Label* Label::create(Theme::Style* style, Properties* properties)
 {
     Label* label = new Label();
     label->initialize(style, properties);
     label->_consumeInputEvents = false;
+    
+    // Ensure that labels cannot receive focus.
+    label->_focusIndex = -2;
 
     return label;
 }

+ 10 - 0
gameplay/src/Label.h

@@ -33,6 +33,16 @@ class Label : public Control
 
 public:
 
+    /**
+     * Create a new label control.
+     *
+     * @param id The control's ID.
+     * @param style The control's style.
+     *
+     * @return The new label.
+     */
+    static Label* create(const char*id, Theme::Style* style);
+
     /**
      * Set the text for this label to display.
      *

+ 12 - 0
gameplay/src/RadioButton.cpp

@@ -24,6 +24,18 @@ RadioButton::~RadioButton()
     }
 }
 
+RadioButton* RadioButton::create(const char* id, Theme::Style* style)
+{
+    GP_ASSERT(style);
+
+    RadioButton* radioButton = new RadioButton();
+    if (id)
+        radioButton->_id = id;
+    radioButton->setStyle(style);
+
+    return radioButton;
+}
+
 RadioButton* RadioButton::create(Theme::Style* style, Properties* properties)
 {
     GP_ASSERT(properties);

+ 11 - 0
gameplay/src/RadioButton.h

@@ -38,6 +38,17 @@ class RadioButton : public Button
     friend class Container;
 
 public:
+
+    /**
+     * Create a new radio button control.
+     *
+     * @param id The control's ID.
+     * @param style The control's style.
+     *
+     * @return The new radio button.
+     */
+    static RadioButton* create(const char* id, Theme::Style* style);
+
     /**
      * Get whether this radio button is currently selected.
      *

+ 69 - 2
gameplay/src/Slider.cpp

@@ -3,6 +3,9 @@
 namespace gameplay
 {
 
+// Fraction of slider to scroll when mouse scrollwheel is used.
+static const float SCROLL_FRACTION = 0.1f;
+
 Slider::Slider() : _minImage(NULL), _maxImage(NULL), _trackImage(NULL), _markerImage(NULL)
 {
 }
@@ -11,6 +14,18 @@ Slider::~Slider()
 {
 }
 
+Slider* Slider::create(const char* id, Theme::Style* style)
+{
+    GP_ASSERT(style);
+
+    Slider* slider = new Slider();
+    if (id)
+        slider->_id = id;
+    slider->setStyle(style);
+
+    return slider;
+}
+
 Slider* Slider::create(Theme::Style* style, Properties* properties)
 {
     GP_ASSERT(properties);
@@ -133,13 +148,65 @@ bool Slider::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contac
 
         if (evt == Touch::TOUCH_RELEASE)
         {
-            _state = NORMAL;
+            _state = FOCUS;
             _dirty = true;
         }
         break;
     }
+    
+    if (evt == Touch::TOUCH_MOVE)
+        return _consumeInputEvents;
+    else
+        return Control::touchEvent(evt, x, y, contactIndex);
+}
+
+bool Slider::mouseEvent(Mouse::MouseEvent evt, int x, int y, int wheelDelta)
+{
+    if (!isEnabled())
+    {
+        return false;
+    }
+
+    switch (evt)
+    {
+        case Mouse::MOUSE_PRESS_LEFT_BUTTON:
+            return touchEvent(Touch::TOUCH_PRESS, x, y, 0);
+
+        case Mouse::MOUSE_MOVE:
+            return touchEvent(Touch::TOUCH_MOVE, x, y, 0);
+
+        case Mouse::MOUSE_RELEASE_LEFT_BUTTON:
+            return touchEvent(Touch::TOUCH_RELEASE, x, y, 0);
+
+        case Mouse::MOUSE_WHEEL:
+        {
+            if (_state == FOCUS)
+            {
+                float total = _max - _min;
+                float oldValue = _value;
+                _value += (total * SCROLL_FRACTION) * wheelDelta;
+            
+                if (_value > _max)
+                    _value = _max;
+                else if (_value < _min)
+                    _value = _min;
+
+                if (_value != oldValue)
+                {
+                    notifyListeners(Listener::VALUE_CHANGED);
+                }
+
+                _dirty = true;
+                return _consumeInputEvents;
+            }
+            break;
+        }
+
+        default:
+            break;
+    }
 
-    return Control::touchEvent(evt, x, y, contactIndex);
+    return false;
 }
 
 void Slider::update(const Control* container, const Vector2& offset)

+ 24 - 0
gameplay/src/Slider.h

@@ -35,6 +35,16 @@ class Slider : public Label
 
 public:
 
+    /**
+     * Create a new slider control.
+     *
+     * @param id The control's ID.
+     * @param style The control's style.
+     *
+     * @return The new slider.
+     */
+    static Slider* create(const char* id, Theme::Style* style);
+
     /**
      * Set the minimum value that can be set on this slider.
      *
@@ -141,6 +151,20 @@ protected:
      */
     bool touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int contactIndex);
 
+    /**
+     * Mouse callback on mouse events.
+     *
+     * @param evt The mouse event that occurred.
+     * @param x The x position of the mouse in pixels. Left edge is zero.
+     * @param y The y position of the mouse in pixels. Top edge is zero.
+     * @param wheelDelta The number of mouse wheel ticks. Positive is up (forward), negative is down (backward).
+     *
+     * @return True if the mouse event is consumed or false if it is not consumed.
+     *
+     * @see Mouse::MouseEvent
+     */
+    bool mouseEvent(Mouse::MouseEvent evt, int x, int y, int wheelDelta);
+
     /**
      * Draw the images associated with this control.
      *

+ 0 - 25
gameplay/src/SpriteBatch.cpp

@@ -256,17 +256,6 @@ void SpriteBatch::draw(const Vector3& position, const Vector3& right, const Vect
     float u1, float v1, float u2, float v2, const Vector4& color, const Vector2& rotationPoint, float rotationAngle)
 {
     // Calculate the vertex positions.
-    //static Vector3 p[4];
-
-    // Pre-optimized:
-    /*
-    p[0] = position - 0.5f * width * right - 0.5f * height * forward;
-    p[1] = position + 0.5f * width * right - 0.5f * height * forward;
-    p[2] = p[0] + height * forward;
-    p[3] = p[1] + height * forward;
-    */
-    
-    // Optimized:
     Vector3 tRight(right);
     tRight *= width * 0.5f;
     Vector3 tForward(forward);
@@ -288,11 +277,6 @@ void SpriteBatch::draw(const Vector3& position, const Vector3& right, const Vect
     p3 += tForward;
 
     // Calculate the rotation point.
-    
-    // Pre-optimized:
-    //Vector3 rp = p[0] + (rotationPoint.x * width * right) + (rotationPoint.y * height * forward);
-
-    // Optimized:
     Vector3 rp = p0;
     tRight = right;
     tRight *= width * rotationPoint.x;
@@ -306,15 +290,6 @@ void SpriteBatch::draw(const Vector3& position, const Vector3& right, const Vect
     static Matrix rotation;
     Matrix::createRotation(u, rotationAngle, &rotation);
 
-    // Pre-optimized:
-    /*
-    p[0] = (rotation * (p[0] - rp)) + rp;
-    p[1] = (rotation * (p[1] - rp)) + rp;
-    p[2] = (rotation * (p[2] - rp)) + rp;
-    p[3] = (rotation * (p[3] - rp)) + rp;
-    */
-
-    // Optimized:
     p0 -= rp;
     p0 *= rotation;
     p0 += rp;

+ 50 - 24
gameplay/src/TextBox.cpp

@@ -16,6 +16,18 @@ TextBox::~TextBox()
 {
 }
 
+TextBox* TextBox::create(const char* id, Theme::Style* style)
+{
+    GP_ASSERT(style);
+
+    TextBox* textBox = new TextBox();
+    if (id)
+        textBox->_id = id;
+    textBox->setStyle(style);
+
+    return textBox;
+}
+
 TextBox* TextBox::create(Theme::Style* style, Properties* properties)
 {
     TextBox* textBox = new TextBox();
@@ -49,36 +61,32 @@ bool TextBox::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int conta
     switch (evt)
     {
     case Touch::TOUCH_PRESS: 
-        if (_state == NORMAL)
-        {
-            _state = ACTIVE;
-            Game::getInstance()->displayKeyboard(true);
-            _dirty = true;
-            return _consumeInputEvents;
-        }
-        else if (x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width &&
+        if (x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width &&
                  y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height)
         {
-            setCaretLocation(x, y);
+            if (_state == NORMAL)
+                Game::getInstance()->displayKeyboard(true);
+            else
+                setCaretLocation(x, y);
+
+            _state = ACTIVE;
             _dirty = true;
-            return _consumeInputEvents;
         }
         else
         {
             _state = NORMAL;
             Game::getInstance()->displayKeyboard(false);
             _dirty = true;
-            return _consumeInputEvents;
+            return false;
         }
         break;
     case Touch::TOUCH_MOVE:
-        if (_state == FOCUS &&
+        if (_state == ACTIVE &&
             x > _clipBounds.x && x <= _clipBounds.x + _clipBounds.width &&
             y > _clipBounds.y && y <= _clipBounds.y + _clipBounds.height)
         {
             setCaretLocation(x, y);
             _dirty = true;
-            return _consumeInputEvents;
         }
         break;
     case Touch::TOUCH_RELEASE:
@@ -87,16 +95,14 @@ bool TextBox::touchEvent(Touch::TouchEvent evt, int x, int y, unsigned int conta
         {
             setCaretLocation(x, y);
             _state = FOCUS;
-            _dirty = true;
-            return _consumeInputEvents;
         }
         else
         {
             _state = NORMAL;
             Game::getInstance()->displayKeyboard(false);
-            _dirty = true;
-            return _consumeInputEvents;
         }
+
+        _dirty = true;
         break;
     }
 
@@ -156,7 +162,7 @@ bool TextBox::keyEvent(Keyboard::KeyEvent evt, int key)
                         Font::Justify textAlignment = getTextAlignment(_state);
                         bool rightToLeft = getTextRightToLeft(_state);
 
-                        unsigned int textIndex = font->getIndexAtLocation(_text.c_str(), _textBounds, fontSize, _caretLocation, &_caretLocation,
+                        int textIndex = font->getIndexAtLocation(_text.c_str(), _textBounds, fontSize, _caretLocation, &_caretLocation,
                             textAlignment, true, rightToLeft);
                         font->getLocationAtIndex(_text.c_str(), _textBounds, fontSize, &_caretLocation, textIndex - 1,
                             textAlignment, true, rightToLeft);
@@ -187,10 +193,15 @@ bool TextBox::keyEvent(Keyboard::KeyEvent evt, int key)
                         unsigned int fontSize = getFontSize(_state);
                         Font::Justify textAlignment = getTextAlignment(_state);
                         bool rightToLeft = getTextRightToLeft(_state);
-
+                        _prevCaretLocation.set(_caretLocation);
                         _caretLocation.y -= fontSize;
-                        font->getIndexAtLocation(_text.c_str(), _textBounds, fontSize, _caretLocation, &_caretLocation,
+                        int textIndex = font->getIndexAtLocation(_text.c_str(), _textBounds, fontSize, _caretLocation, &_caretLocation,
                             textAlignment, true, rightToLeft);
+                        if (textIndex == -1)
+                        {
+                            _caretLocation.set(_prevCaretLocation);
+                        }
+
                         _dirty = true;
                         consume = true;
                         break;
@@ -202,10 +213,15 @@ bool TextBox::keyEvent(Keyboard::KeyEvent evt, int key)
                         unsigned int fontSize = getFontSize(_state);
                         Font::Justify textAlignment = getTextAlignment(_state);
                         bool rightToLeft = getTextRightToLeft(_state);
-
+                        _prevCaretLocation.set(_caretLocation);
                         _caretLocation.y += fontSize;
-                        font->getIndexAtLocation(_text.c_str(), _textBounds, fontSize, _caretLocation, &_caretLocation,
+                        int textIndex = font->getIndexAtLocation(_text.c_str(), _textBounds, fontSize, _caretLocation, &_caretLocation,
                             textAlignment, true, rightToLeft);
+                        if (textIndex == -1)
+                        {
+                            _caretLocation.set(_prevCaretLocation);
+                        }
+
                         _dirty = true;
                         consume = true;
                         break;
@@ -224,6 +240,12 @@ bool TextBox::keyEvent(Keyboard::KeyEvent evt, int key)
 
                 int textIndex = font->getIndexAtLocation(_text.c_str(), _textBounds, fontSize, _caretLocation, &_caretLocation,
                     textAlignment, true, rightToLeft);
+                if (textIndex == -1)
+                {
+                    textIndex = 0;
+                    font->getLocationAtIndex(_text.c_str(), _textBounds, fontSize, &_caretLocation, 0,
+                        textAlignment, true, rightToLeft);
+                }
 
                 switch (key)
                 {
@@ -237,13 +259,17 @@ bool TextBox::keyEvent(Keyboard::KeyEvent evt, int key)
                                 textAlignment, true, rightToLeft);
 
                             _dirty = true;
-                            consume = true;
                         }
+                        consume = true;
                         break;
                     }
                     case Keyboard::KEY_RETURN:
                         // TODO: Handle line-break insertion correctly.
                         break;
+                    case Keyboard::KEY_ESCAPE:
+                        break;
+                    case Keyboard::KEY_TAB:
+                        break;
                     default:
                     {
                         // Insert character into string.
@@ -312,7 +338,7 @@ void TextBox::update(const Control* container, const Vector2& offset)
 
 void TextBox::drawImages(SpriteBatch* spriteBatch, const Rectangle& clip)
 {
-    if (_state == FOCUS)
+    if (_state == ACTIVE || _state == FOCUS)
     {
         // Draw the cursor at its current location.
         GP_ASSERT(_caretImage);

+ 10 - 0
gameplay/src/TextBox.h

@@ -39,6 +39,16 @@ class TextBox : public Label
 
 public:
 
+    /**
+     * Create a new text box control.
+     *
+     * @param id The control's ID.
+     * @param style The control's style.
+     *
+     * @return The new text box.
+     */
+    static TextBox* create(const char* id, Theme::Style* style);
+
     /**
      * Add a listener to be notified of specific events affecting
      * this control.  Event types can be OR'ed together.

+ 29 - 17
gameplay/src/Theme.h

@@ -135,7 +135,6 @@ namespace gameplay
 class Theme: public Ref
 {
     friend class Control;
-    friend class Container;
     friend class Form;
     friend class Skin;
 
@@ -294,6 +293,35 @@ public:
         Vector4 _color;
     };
 
+    /**
+     * Creates a theme using the data from the Properties object defined at the specified URL, 
+     * where the URL is of the format "<file-path>.<extension>#<namespace-id>/<namespace-id>/.../<namespace-id>"
+     * (and "#<namespace-id>/<namespace-id>/.../<namespace-id>" is optional). 
+     * 
+     * @param url The URL pointing to the Properties object defining the theme. 
+     */
+    static Theme* create(const char* url);
+
+    /**
+     * Get a style by its ID.
+     *
+     * @param id The style ID.
+     *
+     * @return The style with the specified ID, or NULL if it does not exist.
+     */
+    Theme::Style* getStyle(const char* id) const;
+
+    /**
+     * Get the empty style.  Used when a control does not specify a style.
+     * This is especially useful for containers that are being used only for
+     * layout and positioning, and have no background or border themselves.
+     * The empty style has no border, background, margin, padding, images, etc..
+     * Any needed properties can be set on the control directly.
+     *
+     * @return The empty style.
+     */
+    Theme::Style* getEmptyStyle();
+
 private:
 
     /**
@@ -406,22 +434,6 @@ private:
      */
     ~Theme();
 
-    /**
-     * Creates an instance of a Theme using the data from the Properties object defined at the specified URL, 
-     * where the URL is of the format "<file-path>.<extension>#<namespace-id>/<namespace-id>/.../<namespace-id>"
-     * (and "#<namespace-id>/<namespace-id>/.../<namespace-id>" is optional). 
-     * 
-     * @param url The URL pointing to the Properties object defining the theme.
-     *
-     * @return A new Theme.
-     */
-    static Theme* create(const char* url);
-
-    Theme::Style* getStyle(const char* id) const;
-
-    // Used when a control does not specify a style.
-    Theme::Style* getEmptyStyle();
-
     void setProjectionMatrix(const Matrix& matrix);
 
     SpriteBatch* getSpriteBatch() const;

+ 5 - 0
gameplay/src/ThemeStyle.cpp

@@ -39,6 +39,11 @@ Theme::Style::~Style()
         SAFE_RELEASE(_overlays[i]);
     }
 }
+
+Theme* Theme::Style::getTheme() const
+{
+    return _theme;
+}
     
 const char* Theme::Style::getId() const
 {

+ 9 - 0
gameplay/src/ThemeStyle.h

@@ -25,6 +25,15 @@ class Theme::Style
     friend class Container;
     friend class Form;
 
+public:
+
+    /**
+     * Get the theme this style belongs to.
+     *
+     * @return The theme this style belongs to.
+     */
+    Theme* getTheme() const;
+
 private:
 
     /**

+ 7 - 9
gameplay/src/VerticalLayout.h

@@ -13,11 +13,16 @@ namespace gameplay
  */
 class VerticalLayout : public Layout
 {
-    friend class Form;
-    friend class Container;
 
 public:
 
+    /**
+     * Create a VerticalLayout.
+     *
+     * @return a VerticalLayout object.
+     */
+    static VerticalLayout* create();
+
     /**
      * Set whether this layout will start laying out controls from the bottom of the container.
      * This setting defaults to 'false', meaning controls will start at the top.
@@ -52,13 +57,6 @@ protected:
      */
     virtual ~VerticalLayout();
 
-    /**
-     * Create a VerticalLayout.
-     *
-     * @return a VerticalLayout object.
-     */
-    static VerticalLayout* create();
-
     /**
      * Update the controls contained by the specified container.
      *