Jump to content

  • Log In with Google      Sign In   
  • Create Account





GUIs - Wheel reinvention

Posted by AaronWizardstar, 04 July 2013 · 552 views

c++ gui madness
In a fit of madness I came up with this:

widget.h and widget.cpp
#ifndef WIDGET_HPP
#define WIDGET_HPP

#include <vector>

#include <SFML/Graphics.hpp>

using std::vector;

class Widget
{
	public:
		enum Visibility
		{
			VISIBLE,
			HIDDEN,
			COLLAPSED
		};

		bool isModal;

		sf::Vector2i pos;

		Widget() : parentPtr(NULL), isModal(false), pos(0, 0),
		  visibility(VISIBLE) {}
		virtual ~Widget() {}

		bool addChild(Widget &w);
		bool removeChild(const Widget &w);

		int childrenCount() const;
		Widget * const getChildPtr(const int index);

		Widget * const getParentPtr();

		// Is w an ancestor of this widget?
		bool hasAncestor(const Widget &ancestor) const;
		// Is w a descendant of this widget?
		bool hasDescendant(const Widget &descendant) const;

		virtual int getWidth() const = 0;
		virtual int getHeight() const = 0;

		virtual bool setWidth(const int newWidth) = 0;
		virtual bool setHeight(const int newHeight) = 0;

		Visibility getVisibility() const;
		void setVisibility(const Visibility newVisibility);

		sf::Vector2i getScreenPos() const;

		bool containsLocalPos(const sf::Vector2i &local) const;
		bool containsScreenPos(const sf::Vector2i &screen) const;

		Widget *mouseDown(const sf::Vector2i &mLocal);
		virtual void mouseMove(const sf::Vector2i &mLocal,
		  const sf::Vector2i &delta) = 0;
		virtual void mouseUp(const sf::Vector2i &mLocal) = 0;

		virtual void keyDown(const sf::Event::KeyEvent keyEvent) = 0;
		virtual void keyUp(const sf::Event::KeyEvent keyEvent) = 0;
		virtual void textInput(const sf::Event::TextEvent textEvent)
		  = 0;

		virtual void gainFocus() = 0;
		virtual void loseFocus() = 0;

		void update();
		void draw() const;

	private:
		vector<Widget *> children;
		Widget *parentPtr;

		Visibility visibility;

		virtual void mouseDownEvent(const sf::Vector2i &mLocal) = 0;

		virtual void doUpdate() = 0;
		virtual void doDraw() const = 0;
};

typedef vector<Widget *>::iterator WidgetIt;
typedef vector<Widget *>::const_iterator ConstWidgetIt;

typedef vector<Widget *>::reverse_iterator WidgetRIt;

#endif
#include "widget.h"

#include <algorithm>

using std::find;

bool Widget::addChild(Widget &w)
{
	bool result = false;

	// A widget is not allowed to become its own child or grandchild
	if ((&w != this) && !hasAncestor(w))
	{
		if ((w.parentPtr != NULL) && (w.parentPtr == this))
		{
			w.parentPtr->removeChild(w);
		}

		children.push_back(&w);
		w.parentPtr = this;

		result = true;
	}

	return result;
}

bool Widget::removeChild(const Widget &w)
{
	bool result = false;

	if (w.parentPtr == this)
	{
		children.erase(find(children.begin(), children.end(), &w));
		result = true;
	}

	return result;
}

int Widget::childrenCount() const
{
	return children.size();
}

Widget * const Widget::getChildPtr(const int index)
{
	return children.at(index);
}

Widget * const Widget::getParentPtr()
{
	return parentPtr;
}

bool Widget::hasAncestor(const Widget &ancestor) const
{
	return (parentPtr != NULL) && ((parentPtr == &ancestor)
	  || (parentPtr->hasAncestor(ancestor)));
}

bool Widget::hasDescendant(const Widget &descendant) const
{
	bool result = false;

	for (ConstWidgetIt it = children.begin(); it != children.end();
	  ++it)
	{
		if ((*it == &descendant)
		  || ((*it)->hasDescendant(descendant)))
		{
			result = true;
			break;
		}
	}

	return result;
}

//--------------------------------------------------------------------

Widget::Visibility Widget::getVisibility() const
{
	return visibility;
}

void Widget::setVisibility(const Visibility newVisibility)
{
	visibility = newVisibility;
}

//--------------------------------------------------------------------

sf::Vector2i Widget::getScreenPos() const
{
	sf::Vector2i result(pos);

	if (parentPtr != NULL)
	{
		result += parentPtr->getScreenPos();
	}

	return pos;
}

bool Widget::containsLocalPos(const sf::Vector2i &local) const
{
	sf::IntRect rect(0, 0, getWidth(), getHeight());
	return rect.contains(local);
}

bool Widget::containsScreenPos(const sf::Vector2i &screen) const
{
	sf::IntRect rect(getScreenPos(),
	  sf::Vector2i(getWidth(), getHeight()));
	return rect.contains(screen);
}

//--------------------------------------------------------------------

Widget *Widget::mouseDown(const sf::Vector2i &mLocal)
{
	Widget *resultPtr = NULL;

	if ((visibility == VISIBLE) && containsLocalPos(mLocal))
	{
		for (WidgetIt it = children.begin(); it != children.end();
		  ++it)
		{
			resultPtr = (*it)->mouseDown(mLocal - (*it)->pos);
			if (resultPtr != NULL)
			{
				break;
			}
		}

		if (resultPtr == NULL)
		{
			resultPtr = this;
			mouseDownEvent(mLocal);
		}
	}

	return resultPtr;
}

//--------------------------------------------------------------------

void Widget::update()
{
	if (visibility == VISIBLE)
	{
		doUpdate();
		for (WidgetIt it = children.begin(); it != children.end();
		  ++it)
		{
			(*it)->update();
		}
	}
}

void Widget::draw() const
{
	if (visibility == VISIBLE)
	{
		// Push (pos.x, pos.y) translation
		...

		doDraw();

		for (ConstWidgetIt it = children.begin();
		  it != children.end(); ++it)
		{
			(*it)->draw();
		}

		// Pop translation
		...
	}
}
gui.h and gui.cpp
#ifndef GUI_HPP
#define GUI_HPP

#include <vector>

#include <SFML/Graphics.hpp>

using std::vector;

class Widget;

class GUI
{
	public:
		GUI() : focusPtr(NULL) {}
		virtual ~GUI() {}

		void clear();

		bool pushWidget(Widget &w);
		bool removeWidget(const Widget &w);

		void clearFocus();
		void setFocus(Widget &w);

		bool mouseDown(const int screenX, const int screenY);
		bool mouseMove(const int screenX, const int screenY,
		  const int dx, const int dy);
		bool mouseUp(const int screenX, const int screenY);

		bool keyDown(const sf::Event::KeyEvent keyEvent);
		bool keyUp(const sf::Event::KeyEvent keyEvent);
		bool textInput(const sf::Event::TextEvent textEvent);

		void update();
		void draw() const;

	private:
		vector<Widget *> widgetPtrStack;
		Widget *focusPtr;

		sf::Vector2i mouseToFocusPos(
		  const int screenX, const int screenY) const;
};

#endif
#include "gui.h"

#include <algorithm>

#include "widget.h"

using std::find;

void GUI::clear()
{
	clearFocus();
	widgetPtrStack.clear();
}

bool GUI::pushWidget(Widget &w)
{
	bool result = false;

	// Only allow root widgets
	if (w.getParentPtr() == NULL)
	{
		removeWidget(w);
		widgetPtrStack.push_back(&w);

		if (w.isModal)
		{
			clearFocus();
		}

		result = true;
	}

	return result;
}

bool GUI::removeWidget(const Widget &w)
{
	bool result = false;

	WidgetIt it = find(widgetPtrStack.begin(), widgetPtrStack.end(),
	  &w);
	if (it != widgetPtrStack.end())
	{
		if (*it == focusPtr)
		{
			clearFocus();
		}

		widgetPtrStack.erase(it);
		result = true;
	}

	return result;
}

void GUI::clearFocus()
{
	if (focusPtr != NULL)
	{
		focusPtr->loseFocus();
	}
	focusPtr = NULL;
}

void GUI::setFocus(Widget &w)
{
	if (focusPtr != &w)
	{
		clearFocus();
		focusPtr = &w;
		focusPtr->gainFocus();
	}
}

//--------------------------------------------------------------------

bool GUI::mouseDown(const int screenX, const int screenY)
{
	bool result = false;

	for (WidgetRIt it = widgetPtrStack.rbegin();
	  it != widgetPtrStack.rend(); ++it)
	{
		sf::Vector2i mLocal = sf::Vector2i(screenX, screenY)
		  - (*it)->pos;
		Widget *newFocus = (*it)->mouseDown(mLocal);

		if ((newFocus == NULL) && (*it)->isModal)
		{
			break;
		}
		else if (newFocus != NULL)
		{
			setFocus(*newFocus);
			result = true;
			break;
		}
	}
	if (!result)
	{
		clearFocus();
	}

	return result;
}

bool GUI::mouseMove(const int screenX, const int screenY,
  const int dx, const int dy)
{
	bool result = false;
	if (focusPtr != NULL)
	{
		focusPtr->mouseMove(mouseToFocusPos(screenX, screenY),
		  sf::Vector2i(dx, dy));
		result = true;
	}
	return result;
}

bool GUI::mouseUp(const int screenX, const int screenY)
{
	bool result = false;
	if (focusPtr != NULL)
	{
		focusPtr->mouseUp( mouseToFocusPos(screenX, screenY) );
		result = true;
	}
	return result;
}

sf::Vector2i GUI::mouseToFocusPos(
  const int screenX, const int screenY) const
{
	return sf::Vector2i(screenX, screenY) - focusPtr->getScreenPos();
}

//--------------------------------------------------------------------

bool GUI::keyDown(const sf::Event::KeyEvent keyEvent)
{
	bool result = false;
	if (focusPtr != NULL)
	{
		focusPtr->keyDown(keyEvent);
		result = true;
	}
	return result;
}

bool GUI::keyUp(const sf::Event::KeyEvent keyEvent)
{
	bool result = false;
	if (focusPtr != NULL)
	{
		focusPtr->keyUp(keyEvent);
		result = true;
	}
	return result;
}

bool GUI::textInput(const sf::Event::TextEvent textEvent)
{
	bool result = false;
	if (focusPtr != NULL)
	{
		focusPtr->textInput(textEvent);
		result = true;
	}
	return result;
}

//--------------------------------------------------------------------

void GUI::update()
{
	for (WidgetIt it = widgetPtrStack.begin();
	  it != widgetPtrStack.end(); ++it)
	{
		(*it)->update();
	}
}

void GUI::draw() const
{
	for (ConstWidgetIt it = widgetPtrStack.begin();
	  it != widgetPtrStack.end(); ++it)
	{
		(*it)->draw();
	}
}
This is my take on an ostensibly light-yet-flexible core of a GUI system.

Everything is a Widget. Widgets can contain other Widgets, making trees of Widgets.

Widget trees are handled by a GUI manager. A GUI manager takes a root Widget and feeds it input events, updates it, and renders it. A GUI manager actually handles a stack of root Widgets to represent overlapping UIs to represent things like modal forms and pop-up menus.

The GUI manager also keeps track of the current focus, needed for keyboard events. A Widget, wherever it is in its tree, gains focus when clicked on. Focus is lost when an empty space is clicked.

I need to finalize how I'm going to handle Widget sizes. You'd be allowed to set the dimensions of most Widgets, but some Widgets like icons may have fixed sizes. I'd also want default sizing behaviour for certain types of Widgets, like containers that grow and shrink to fit their child Widgets, that would end when you set a specific size on the Widget.
Widgets also need to notify their parent when their dimensions, and their visibility, change. I'll likely do this through a callback like childResized().




What's the difference between draw and doDraw? Instead of declaring ConstWidgetIt etc you could use the auto keyword. I use STL lists instead of vectors because a vector array has to be reallocated and each element reassigned each time an element is pushed.

draw is the public method where the Widget draws itself then its children. draw calls doDraw, which is the private virtual method that is meant to do the actual drawing of the Widget itself.

 

My machine doesn't have C++11 on it so I can't use the auto keyword like that even if I wanted to. Though I don't really mind specifically defining types in a language like C++, so I suppose it's a style thing.

 

A case could be made for using a list instead of a vector for storing the child widget, though sometimes I do want to get a child by index (i.e. getChildPtr) and I don't believe adding items to a vector is that expensive in practise for most STL implementations.

Shouldn't doDraw() and doUpdate() be protected?

Having pure-virtual private member functions seems almost pointless. :P

 

Jokes aside, it seems like a really reasonable and sane implementation.

@polyfrag: Elements don't have to be reassigned every time an element is pushed - only when more memory is needed. If I push five elements and pop two, then my next two pushes will use the spots vacated by those I previously popped.

 

Many implementations also allocate a little extra space during each reallocation, and if you know ahead roughly how many elements you are expecting, you can call std::vector<>::reserve(). Even if you're off by a little, you'll still likely invoke only one reallocation rather than dozens.

 

Here's the output of a GCC test I just did. Over 100 pushes, only 8 reallocations. Frankly, I'm surprised it didn't start out allocating 10 or so elements - the first five reallocations could've been saved. Even so, it still did a good job. Every reallocation allocated twice the amount of the prior capacity. I've heard the Visual Studio found a 33% increase to be an even better balance between speed and memory optimization.

 

I use vectors as my default go-to container for just about everything. smile.png

(This is because I access way more frequently than I push - so I go with the container optimized for access, unless profiling a specific real-world slowdown tells me to change to something else)

Shouldn't doDraw() and doUpdate() be protected?

Having pure-virtual private member functions seems almost pointless. tongue.png

It's valid in C++ to have a private virtual method. The method just can't be called in any methods of a derived class.

 

That said, the Widget class would be embedded into Squirrel. The virtual methods wouldn't actually be virtual at all; they'd just call the corresponding methods of the Squirrel Widget object. Squirrel would be where all the Widget subclasses live.

a vector array has to be reallocated and each element reassigned each time an element is pushed.

A vector only reallocates when it is full (when size() == capacity()), when it reallocates it doesn't just grow its capacity by 1, it actually allocates a bunch of extra space (often it doubles in capacity) so that most of the time when you push a new element in there is already memory allocated for it making it very cheap to add elements in the general case.
 

Having pure-virtual private member functions seems almost pointless.

Pointless or best-practice? tongue.png Take a look at Guideline #2 from Herb Sutter: http://www.gotw.ca/publications/mill18.htm
 

@AaronWizardstar: Good start. I have never written my own GUI system (although I would find it a very interesting project for sure) but I reckon sizing and layout will get pretty gnarly. Some things that strike me are:

  • Min-max dimensions for things that can grow and shrink
  •  
  • Preferred-versus-actual dimensions (e.g. an image having preferred dimensions of 100x100 but being squashed to 10x10).
  •  
  • Anchors for associating edges of a widget with its parent's edges so that widgets can grow to occupy the space of their parent (if all edges where anchored, or maybe only horizontal/vertical edges), or stay tethered to to one side/corner of their parent.
  •  
  • You also need to decide how you want to model margins (whitespace surrounding widgets) w.r.t sizing. Sometimes margins are modelled as a property of the widget (e.g. CSS) and sometimes it's just part of the layout-spacing for the parent's layout-manager.

Search My Journal

October 2014 »

S M T W T F S
   1234
567891011
12131415161718
1920 21 22232425
262728293031 

Recent Comments

PARTNERS