Game Development Community

A better guiSliderCtrl

by Konrad Kiss · in Torque 3D Professional · 06/15/2009 (1:16 pm) · 9 replies

The slider control in Torque is pretty cool. I use it extensively for Xenocell. I wanted to share my version that extends the existing one while staying compatible with it. The bonus features are:

- thumb size is automatically found from the bitmap array, so it will always be positioned well
- thumb repositioning upon resizing
- snap or smooth mode regardless of SHIFT's state
- 0 ticks and snap mode possible
- fixed a small bug/typo with ticks and snap mode
- thumb highlight not only on mouseover but also on mousedrag

I wasn't sure where to post it, so I decided to keep it here. It should be compatible with previous settings of the slider control, so it should not break your existing gui. I marked my changes in there - in case you're interested. (// >>> ... // <<<)

guiSliderCtrl.h
//-----------------------------------------------------------------------------
// Torque 3D
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#ifndef _GUISLIDERCTRL_H_
#define _GUISLIDERCTRL_H_

#ifndef _GUICONTROL_H_
#include "gui/core/guiControl.h"
#endif

class GuiSliderCtrl : public GuiControl
{
private:
   typedef GuiControl Parent;

protected:
   Point2F mRange;
   U32  mTicks;
   F32  mValue;
   RectI   mThumb;
   Point2I mThumbSize;
   void updateThumb( F32 value, bool snap = true, bool onWake = false );
   S32 mShiftPoint;
   S32 mShiftExtent;
   F32 mIncAmount;
   bool mDisplayValue;
   bool mDepressed;
   bool mMouseOver;
	bool mMouseDragged; // >>> <<<
	bool mSnap; // >>> <<<
   bool mHasTexture;

   enum
   {
	   SliderLineLeft = 0,
	   SliderLineCenter,
	   SliderLineRight,
	   SliderButtonNormal,
	   SliderButtonHighlight,
	   NumBitmaps
   };
   	RectI *mBitmapBounds;

public:
   //creation methods
   DECLARE_CONOBJECT(GuiSliderCtrl);
   GuiSliderCtrl();
   static void initPersistFields();

	// >>>
   void updateSize();
   virtual void parentResized(const RectI& oldParentRect, const RectI& newParentRect);
	// <<<

	//Parental methods
   bool onWake();

   void onMouseDown(const GuiEvent &event);
   void onMouseDragged(const GuiEvent &event);
   void onMouseUp(const GuiEvent &);
   void onMouseLeave(const GuiEvent &);
   void onMouseEnter(const GuiEvent &);
   bool onMouseWheelUp(const GuiEvent &event);
   bool onMouseWheelDown(const GuiEvent &event);

	// >>>
   F32 getThumbValue(const GuiEvent &event);
	// <<<

	const Point2F& getRange() const { return mRange; }
   F32 getValue() const { return mValue; }
   void setScriptValue(const char *val) { setValue(dAtof(val)); }
   void setValue(F32 val);

   void onRender(Point2I offset, const RectI &updateRect);
};

#endif

guiSliderCtrl.cpp
//-----------------------------------------------------------------------------
// Torque 3D
// Copyright (C) GarageGames.com, Inc.
//-----------------------------------------------------------------------------

#include "console/console.h"
#include "console/consoleTypes.h"
#include "gfx/gfxTextureManager.h"
#include "gui/controls/guiSliderCtrl.h"
#include "gui/core/guiDefaultControlRender.h"
#include "platform/event.h"
#include "gfx/primBuilder.h"
#include "gfx/gfxDrawUtil.h"
#include "sfx/sfxSystem.h"

IMPLEMENT_CONOBJECT(GuiSliderCtrl);

//----------------------------------------------------------------------------
GuiSliderCtrl::GuiSliderCtrl(void)
{
   mActive = true;
   mRange.set( 0.0f, 1.0f );
   mTicks = 10;
   mValue = NULL; // >>> <<<
   mThumbSize.set(8,20);
   mShiftPoint = 5;
   mShiftExtent = 10;
   mIncAmount = 0.0f;
   mDisplayValue = false;
   mMouseOver = false;
   mDepressed = false;
	// >>>
   mMouseDragged = false; 
	mSnap = false;
	// <<<
}

//----------------------------------------------------------------------------
void GuiSliderCtrl::initPersistFields()
{
   Parent::initPersistFields();

   addGroup( "Slider" );
   addField("range", TypePoint2F,   Offset(mRange, GuiSliderCtrl));
   addField("ticks", TypeS32,       Offset(mTicks, GuiSliderCtrl));
   addField("value", TypeF32,       Offset(mValue, GuiSliderCtrl));
   addField("snap",  TypeBool,      Offset(mSnap,  GuiSliderCtrl));
   endGroup( "Slider" );
}

//----------------------------------------------------------------------------
ConsoleMethod( GuiSliderCtrl, getValue, F32, 2, 2, "Get the position of the slider.")
{
   return object->getValue();
}

//----------------------------------------------------------------------------
void GuiSliderCtrl::setValue(F32 val)
{
   mValue = val;
   updateThumb(mValue, false);
}

//----------------------------------------------------------------------------
bool GuiSliderCtrl::onWake()
{
   if (! Parent::onWake())
      return false;

	// >>>
   mHasTexture = mProfile->constructBitmapArray() >= NumBitmaps;  
	if( mHasTexture ) {
      mBitmapBounds = mProfile->mBitmapArrayRects.address();
		mThumbSize = Point2I(mBitmapBounds[SliderButtonNormal].extent.x, mBitmapBounds[SliderButtonNormal].extent.y);
	}

	if (mValue)
		mValue = mClampF(mValue, mRange.x, mRange.y);
	else
		mValue = mClampF(getFloatVariable(), mRange.x, mRange.y);
	// <<<

   // mouse scroll increment percentage is 5% of the range
   mIncAmount = ((mRange.y - mRange.x) * 0.05);

   if(mThumbSize.y + mProfile->mFont->getHeight()-4 <= getExtent().y)
      mDisplayValue = true;
   else
      mDisplayValue = false;

   updateThumb( mValue, false, true );
	// <<<

   return true;
}

// >>>
void GuiSliderCtrl::updateSize()
{
	// we need to reposition the thumb
	updateThumb( mValue, false );
}

void GuiSliderCtrl::parentResized(const RectI &oldParentRect, const RectI &newParentRect)
{
   Parent::parentResized( oldParentRect, newParentRect );

   updateSize();
}
// <<<

//----------------------------------------------------------------------------
void GuiSliderCtrl::onMouseDown(const GuiEvent &event)
{
   if ( !mActive || !mAwake || !mVisible )
      return;

   mouseLock();
   setFirstResponder();
   mDepressed = true;

   Point2I curMousePos = globalToLocalCoord(event.mousePoint);
   F32 value;
   if (getWidth() >= getHeight())
      value = F32(curMousePos.x-mShiftPoint) / F32(getWidth()-mShiftExtent)*(mRange.y-mRange.x) + mRange.x;
   else
      value = F32(curMousePos.y) / F32(getHeight())*(mRange.y-mRange.x) + mRange.x;
   
   updateThumb( value, ( event.modifier & SI_SHIFT ) );
}

//----------------------------------------------------------------------------
void GuiSliderCtrl::onMouseDragged(const GuiEvent &event)
{
   if ( !mActive || !mAwake || !mVisible )
      return;
	
	// >>>
   mMouseDragged = true;

   Con::executef(this, "onMouseDragged");

	F32 value = getThumbValue(event);
   updateThumb( value, ( event.modifier & SI_SHIFT ) );
	// <<<
}

//----------------------------------------------------------------------------
void GuiSliderCtrl::onMouseUp(const GuiEvent &event)
{
   if ( !mActive || !mAwake || !mVisible )
      return;
   mDepressed = false;
	mMouseDragged = false;
   mouseUnlock();
	// >>>
	F32 value = getThumbValue(event);
	updateThumb( value, ( event.modifier & SI_SHIFT ) );
	// <<<
   execConsoleCallback();
}

// >>>
F32 GuiSliderCtrl::getThumbValue(const GuiEvent &event)
{
   Point2I curMousePos = globalToLocalCoord(event.mousePoint);
   F32 value;
   if (getWidth() >= getHeight())
      value = F32(curMousePos.x-mShiftPoint) / F32(getWidth()-mShiftExtent)*(mRange.y-mRange.x) + mRange.x;
   else
      value = F32(curMousePos.y) / F32(getHeight())*(mRange.y-mRange.x) + mRange.x;

   if (value > mRange.y)
      value = mRange.y;
   else if (value < mRange.x)
      value = mRange.x;

   if ((!(event.modifier & SI_SHIFT) && mTicks >= 1) || mSnap) { // >>> <<<
      // If the shift key is held, snap to the nearest tick, if any are being drawn

      F32 tickStep = (mRange.y - mRange.x) / F32(mTicks + 1);

      F32 tickSteps = (value - mRange.x) / tickStep;
      S32 actualTick = S32(tickSteps + 0.5);

      value = actualTick * tickStep + mRange.x;
      AssertFatal(value <= mRange.y && value >= mRange.x, "Error, out of bounds value generated from shift-snap of slider");
   }

	return value;
}
// <<<

void GuiSliderCtrl::onMouseEnter(const GuiEvent &event)
{
   setUpdate();
   if(isMouseLocked())
   {
      mDepressed = true;
      mMouseOver = true;
   }
   else
   {
      if ( mActive && mProfile->mSoundButtonOver )
      {
         //F32 pan = (F32(event.mousePoint.x)/F32(getRoot()->getWidth())*2.0f-1.0f)*0.8f;
         SFX->playOnce(mProfile->mSoundButtonOver);
      }
      mMouseOver = true;
   }
}

void GuiSliderCtrl::onMouseLeave(const GuiEvent &)
{
   setUpdate();
   if(isMouseLocked())
      mDepressed = false;
   mMouseOver = false;
}
//----------------------------------------------------------------------------
bool GuiSliderCtrl::onMouseWheelUp(const GuiEvent &event)
{
   if ( !mActive || !mAwake || !mVisible )
      return Parent::onMouseWheelUp(event);

   mValue += mIncAmount;
   updateThumb( mValue, ( event.modifier & SI_SHIFT ) );

   return true;
}

bool GuiSliderCtrl::onMouseWheelDown(const GuiEvent &event)
{
   if ( !mActive || !mAwake || !mVisible )
      return Parent::onMouseWheelUp(event);

   mValue -= mIncAmount;
   updateThumb( mValue, ( event.modifier & SI_SHIFT ) );

   return true;
}
//----------------------------------------------------------------------------
void GuiSliderCtrl::updateThumb( F32 _value, bool snap, bool onWake )
{
   if ((snap && mTicks >= 1) || mSnap) { // >>> <<<
      // If the shift key is held, snap to the nearest tick, if any are being drawn

      F32 tickStep = (mRange.y - mRange.x) / F32(mTicks + 1);

      F32 tickSteps = (_value - mRange.x) / tickStep;
      S32 actualTick = S32(tickSteps + 0.5);

      _value = actualTick * tickStep + mRange.x;
   }
   
   mValue = _value;
   // clamp the thumb to legal values
   if (mValue < mRange.x)  mValue = mRange.x;
   if (mValue > mRange.y)  mValue = mRange.y;

   Point2I ext = getExtent();
	ext.x -= ( mShiftExtent + mThumbSize.x ) / 2;
   // update the bounding thumb rect
   if (getWidth() >= getHeight())
   {  // HORZ thumb
      S32 mx = (S32)((F32(ext.x) * (mValue-mRange.x) / (mRange.y-mRange.x)));
      S32 my = ext.y/2;
      if(mDisplayValue)
         my = mThumbSize.y/2;

      mThumb.point.x  = mx - (mThumbSize.x/2);
      mThumb.point.y  = my - (mThumbSize.y/2);
      mThumb.extent   = mThumbSize;
   }
   else
   {  // VERT thumb
      S32 mx = ext.x/2;
      S32 my = (S32)((F32(ext.y) * (mValue-mRange.x) / (mRange.y-mRange.x)));
      mThumb.point.x  = mx - (mThumbSize.y/2);
      mThumb.point.y  = my - (mThumbSize.x/2);
      mThumb.extent.x = mThumbSize.y;
      mThumb.extent.y = mThumbSize.x;
   }
   setFloatVariable(mValue);
   setUpdate();

   // Use the alt console command if you want to continually update:
   if ( !onWake )
      execAltConsoleCallback();
}

//----------------------------------------------------------------------------
void GuiSliderCtrl::onRender(Point2I offset, const RectI &updateRect)
{
   Point2I pos(offset.x+mShiftPoint, offset.y);
   Point2I ext(getWidth() - mShiftExtent, getHeight());
   RectI thumb = mThumb;

   if( mHasTexture )
   {
      if(mTicks > 0)
      {
         // TODO: tick marks should be positioned based on the bitmap dimensions.
         Point2I mid(ext.x, ext.y/2);
         Point2I oldpos = pos;
         pos += Point2I(1, 0);

         PrimBuild::color4f( 0.f, 0.f, 0.f, 1.f );
         PrimBuild::begin( GFXLineList, ( mTicks + 2 ) * 2 );
         // tick marks
         for (U32 t = 0; t <= (mTicks+1); t++)
         {
            S32 x = (S32)(F32(mid.x+1)/F32(mTicks+1)*F32(t)) + pos.x;
            S32 y = pos.y + mid.y;
            PrimBuild::vertex2i(x, y + mShiftPoint);
            PrimBuild::vertex2i(x, y + mShiftPoint*2 + 2);
         }
         PrimBuild::end();
         // TODO: it would be nice, if the primitive builder were a little smarter,
         // so that we could change colors midstream.
         PrimBuild::color4f(0.9f, 0.9f, 0.9f, 1.0f);
         PrimBuild::begin( GFXLineList, ( mTicks + 2 ) * 2 );
         // tick marks
         for (U32 t = 0; t <= (mTicks+1); t++)
         {
            S32 x = (S32)(F32(mid.x+1)/F32(mTicks+1)*F32(t)) + pos.x + 1;
            S32 y = pos.y + mid.y + 1;
            PrimBuild::vertex2i(x, y + mShiftPoint );
            PrimBuild::vertex2i(x, y + mShiftPoint * 2 + 3);
         }
         PrimBuild::end();
         pos = oldpos;
      }

      S32 index = SliderButtonNormal;
      if(mMouseOver || mMouseDragged) // >>> <<<
         index = SliderButtonHighlight;
      GFX->getDrawUtil()->clearBitmapModulation();

      //left border
      GFX->getDrawUtil()->drawBitmapSR(mProfile->mTextureObject, Point2I(offset.x,offset.y), mBitmapBounds[SliderLineLeft]);
      //right border
      GFX->getDrawUtil()->drawBitmapSR(mProfile->mTextureObject, Point2I(offset.x + getWidth() - mBitmapBounds[SliderLineRight].extent.x, offset.y), mBitmapBounds[SliderLineRight]);


      //draw our center piece to our slider control's border and stretch it
      RectI destRect;	
      destRect.point.x = offset.x + mBitmapBounds[SliderLineLeft].extent.x;
      destRect.extent.x = getWidth() - mBitmapBounds[SliderLineLeft].extent.x - mBitmapBounds[SliderLineRight].extent.x;
      destRect.point.y = offset.y;
      destRect.extent.y = mBitmapBounds[SliderLineCenter].extent.y;

      RectI stretchRect;
      stretchRect = mBitmapBounds[SliderLineCenter];
      stretchRect.inset(1,0);

      GFX->getDrawUtil()->drawBitmapStretchSR(mProfile->mTextureObject, destRect, stretchRect);

      //draw our control slider button	
      thumb.point += pos;
      GFX->getDrawUtil()->drawBitmapSR(mProfile->mTextureObject,Point2I(thumb.point.x,offset.y ),mBitmapBounds[index]);

   }
   else if (getWidth() >= getHeight())
   {
      Point2I mid(ext.x, ext.y/2);
      if(mDisplayValue)
         mid.set(ext.x, mThumbSize.y/2);

      PrimBuild::color4f( 0.f, 0.f, 0.f, 1.f );
      PrimBuild::begin( GFXLineList, ( mTicks + 2 ) * 2 + 2);
         // horz rule
         PrimBuild::vertex2i( pos.x, pos.y + mid.y );
         PrimBuild::vertex2i( pos.x + mid.x, pos.y + mid.y );

         // tick marks
         for( U32 t = 0; t <= ( mTicks + 1 ); t++ )
         {
            S32 x = (S32)( F32( mid.x - 1 ) / F32( mTicks + 1 ) * F32( t ) );
            PrimBuild::vertex2i( pos.x + x, pos.y + mid.y - mShiftPoint );
            PrimBuild::vertex2i( pos.x + x, pos.y + mid.y + mShiftPoint );
         }
         PrimBuild::end();
   }
   else
   {
      Point2I mid(ext.x/2, ext.y);

      PrimBuild::color4f( 0.f, 0.f, 0.f, 1.f );
      PrimBuild::begin( GFXLineList, ( mTicks + 2 ) * 2 + 2);
         // horz rule
         PrimBuild::vertex2i( pos.x + mid.x, pos.y );
         PrimBuild::vertex2i( pos.x + mid.x, pos.y + mid.y );

         // tick marks
         for( U32 t = 0; t <= ( mTicks + 1 ); t++ )
         {
            S32 y = (S32)( F32( mid.y - 1 ) / F32( mTicks + 1 ) * F32( t ) );
            PrimBuild::vertex2i( pos.x + mid.x - mShiftPoint, pos.y + y );
            PrimBuild::vertex2i( pos.x + mid.x + mShiftPoint, pos.y + y );
         }
         PrimBuild::end();
      mDisplayValue = false;
   }
   // draw the thumb
   thumb.point += pos;
   renderRaisedBox(thumb, mProfile);

   if(mDisplayValue)
   {
   	char buf[20];
  		dSprintf(buf,sizeof(buf),"%0.3f",mValue);

   	Point2I textStart = thumb.point;

      S32 txt_w = mProfile->mFont->getStrWidth((const UTF8 *)buf);

   	textStart.x += (S32)((thumb.extent.x/2.0f));
   	textStart.y += thumb.extent.y - 2; //19
   	textStart.x -=	(txt_w/2);
   	if(textStart.x	< offset.x)
   		textStart.x = offset.x;
   	else if(textStart.x + txt_w > offset.x+getWidth())
   		textStart.x -=((textStart.x + txt_w) - (offset.x+getWidth()));

    	GFX->getDrawUtil()->setBitmapModulation(mProfile->mFontColor);
    	GFX->getDrawUtil()->drawText(mProfile->mFont, textStart, buf, mProfile->mFontColors);
   }
   renderChildControls(offset, updateRect);
}

#1
06/16/2009 (2:16 am)
Very cool Konrad!

This won't make it into Beta 3 most likely (too close to the cut off) but I'll flag it for Beta 4.
#2
06/17/2009 (12:09 am)
Ooo... this is nice Konrad. Thanks!
#3
03/06/2010 (11:43 am)
does it also fix the mouse capture problem where if your sloppy (like me) and drag the mouse off the slider when turning it up the graphic changes but it doesn't trigger the command?
#4
03/06/2010 (12:48 pm)
@George: I can't say for sure. When the mouse slides off the control while it is being dragged, and then the mouse button is released, then it should trigger the command. Although... if you're using Torque 3D 1.1 Beta 1, some - probably most - of the fixes included in this resource were rolled into that version. There are still a few things you could port over from this resource though.

Be sure to not just drop it into 1.1 Beta 1, as it will cause you more problems than it will solve. For any of the earlier versions, I recommend using this over the one in the source. For 1.1 Beta 1 and higher, you should stick to the version in the source, and modify that to your needs instead.
#5
03/06/2010 (3:12 pm)
Ahh I see I am using the alpha 1.1 I will mess with it and see what I come up with.

Just a side note I think the onMouseLeave sets mdepressed to false with out triggering any updates. I'm kinda new at this so maybe I don't see it correctly. (or the code I'm working with is totally different and I should be looking at it instead of the post.
line 213

Also I just tested the stock functionality and the mouse scroll to move the pointer dosn't trigger the command either. Guess I will add a post in the bug section.

Thanks for this post
#6
03/06/2010 (3:36 pm)
No problem at all.

Edit: I see you just edited your comment.. This is a reply to your original one. :)

I don't think it should be so. As you said previously, it's not always possible to keep the mouse above a control. Your suggestion would push an update and execute the console callback even while you drag the control's thumb while the mouse wanders away from above the control.

On the other hand, mMouseOver would have to be false regardless of isMouseLocked() imho, so:

void GuiSliderCtrl::onMouseLeave(const GuiEvent &)
{
   setUpdate();
   if(isMouseLocked())
      mDepressed = false;
   mMouseOver = false;
}

It would be better if you described what behavior you were looking for exactly - what you intend to use the control for. Maybe we can make use of something that's already implemented.
#7
03/06/2010 (4:00 pm)
I have the sliders connected to a combat system where you can adjust your method of attack by the slider.

The problem is that the command only triggers if you mouseup while directly over the thumb. To make it worse the thumb moves with your mouse if your over it or not.
So if you are needing to make a quick change in the middle of a fight (not to that yet but I see it down the road) and you move the slider it looks like it changed but if your mouse wasn't directly over it on mouse up it never fires the command that does the real work.

#8
03/06/2010 (4:53 pm)
Tried a couple of changes and here is the one I like best so far. Haven't seen any buggy behavior(yet).

Ahh I see yours is not at all like the stock onMouseUp so you don't have that problem.

void GuiSliderCtrl::onMouseUp(const GuiEvent &)
{
if ( !mActive || !mAwake || !mVisible )
return;

if( mDepressed )
{
mDepressed = false;

}
execConsoleCallback(); // moved from inside the above if statment
mouseUnlock();
}

Hey thanks for your help.
#9
03/13/2010 (12:18 am)

Adapted for b2. Thanks Konrad!