Allegro.cc - Online Community

Allegro.cc Forums » Game Design & Concepts » Core GUI design

This thread is locked; no one can reply to it. rss feed Print
Core GUI design
Edgar Reynaldo
Major Reynaldo
May 2007
avatar

I'm in the middle of doing an overhaul on my gui core. I'm rethinking my classes for WidgetBase, WidgetArea, WidgetHandler, and LayoutBase. I'm trying to cut out all the cruft and streamline the interface, removing unnecessary and unused 'features'.

What features should the core GUI support? I'm thinking base class and interface here. HTML has got me thinking about borders, margin, and padding. Every HTML element is capable of having those three things, so I was thinking of putting them all into my WidgetArea class. I've also debated whether or not I should delegate those abilities into a Decorator class. I plan to have sa Decorator class, but I'm not sure how to implement it yet. It has to be a widget, and it should 'decorate' another widget. If I don't add those things into the WidgetBase class, then they have to be implemented as decorators. But the idea of having to add 3 decorators to every widget to achieve HTML like layout seems like a bad idea.

This decorator thing, I just don't quite have the handle on it yet. What's the best way to do this kind of thing?

WidgetBase* w1 = new BorderDecorator(
                    new PaddingDecorator(
                       new MarginDecorator(
                          new IconButton()))));

vs.

WidgetBase* w2 = new IconButton();
w2->SetMargins(...);
w2->SetBorder(...);
w2->SetPadding(...);

Personally I like the second better. It avoids duplication of widget members and achieves the same thing.

I'm getting rid of memory management and using shared_ptr from C++11 now that it is standard and I don't have to use Boost. Yes, I'm a few years behind.

Widgets also currently have WidgetColorsets, BackgroundPainters, and FocusPainters. They all seem somewhat extranneous except for the color set, but I wanted to be able to provide a way to 'skin' the widgets, allowing for setting a background image and different borders. Again, are these 'features' or 'fluff'?

Credits in my code for anyone who helps contribute ideas, design, or code.

What do you want to see in your GUI?

Edgar

bamccaig
Member #7,536
July 2006
avatar

I suggest keeping things a bit simpler. I don't think that good designs come from complex OO. The sort of brilliance of HTML elements is that they're all basically the same. They are just a node with a rich set of style attributes that can be applied. An HTML element exposed to the user might be composed of some subelements, but ultimately it's just a rich set of attributes to describe the position, size, color, border, padding, margin, etc.

I think that you should throw away the idea of "objects". You have "data" which is a set of attributes to describe how to draw a thing, and perhaps a few lists of event handlers to handle state changes and interactions.

I think it's important for there to be an "overseer" of the entire system to understand the size/position of each element. It all comes down to standard attributes. At least, at the macro/widget scale. Inside of a widget, you may need some custom drawing code, and that should be custom, but think of that as a single "dynamic" function with limited access to things. Objects are overly complex. Ultimately, objects are just state with functions attached. And it's overly rigid to bundle it into all into a class. Try to abstract the concepts out so that you don't have to repeat yourself. Objects tend to repeat themselves because they're too rigid.

Interfaces AKA abstract base classes are ideal because it forces you to design in the abstract before forcing existing decisions on the design. This can often help to realize better designs. The other advice that I would give is to keep functions short, and reuse as much as you can. Keep responsibilities short. Even if you end up needing to pass a lot of functions or objects around, it will help the design in the end. Parameters can be changed. Interfaces are often rigid. Object parameters with methods are basically function pointers in disguise. And when these do the work of a function they can transform the behavior of a function. Call it with one object/function and it might write text to a file, but call it with another object/function and it might navigate a space vehicle.

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

bamccaig said:

The sort of brilliance of HTML elements is that they're all basically the same. They are just a node with a rich set of style attributes that can be applied.The sort of brilliance of HTML elements is that they're all basically the same. They are just a node with a rich set of style attributes that can be applied.

So do you suggest moving away from composition in favor of some kind of attribute value mapping? EDIT This is actually what I'm doing in my scripting system. Moving everything into attributes, because they're all strings anyway.

#SelectExpand
1class WidgetBase { 2 typedef std::string ATTRIBUTE; 3 typedef std::string VALUE; 4 typedef std::map<ATTRIBUTE , VALUE> ATTVALMAP; 5 6 ATTVALMAP attributes; 7... 8 virtual bool SetAttribute(ATTRIBUTE , VALUE); 9... 10}; 11 12bool WidgetBase::SetAttribute(ATTRIBUTE a , VALUE v) { 13 attributes[a] = v; 14 if (a.compare("Padding") == 0) { 15 RecalculateArea(); 16 } 17} 18 19WidgetBase w; 20w.SetAttribute("Padding" , "5 10 5 10");

And then expect the WidgetBase class or the derived method to know what to do with the attribute?

bamccaig said:

I think it's important for there to be an "overseer" of the entire system to understand the size/position of each element.

There is, there is the LayoutBase, which controls the position, and the WidgetHandler, which controls all the mundane tasks of managing everything.

bamccaig said:

"
Interfaces AKA abstract base classes are ideal because it forces you to design in the abstract before forcing existing decisions on the design. This can often help to realize better designs. The other advice that I would give is to keep functions short, and reuse as much as you can. Keep responsibilities short.

This is what I'm working towards.

bamccaig
Member #7,536
July 2006
avatar

What I have in mind is more so that the overall GUI is drawn by a single service object, at least at a high level. A "renderer" or whatever you would like to call it. Basically, it's job is to take the data from the widgets/attributes and layout and, using them, calculate the size/position of each widget and draw it there. Drawing the actual widget might involve some custom code for the widget itself, but I prefer to think of that as dynamic data too rather than static-code. Static code is very difficult to extend or modify. Particularly for a library user. Dynamic code can be edited or replaced.

In any case, I think that software is meant to be very malleable. Performance is important too, but I think it's less important than being malleable. It should be easy to adapt the software to your needs. If it can't do what you need it to being fast doesn't matter. On the other hand, if it does what you need, but is slower than a human then it might as well be put back into paper and pen. So there needs to be a balance, but too often software is so rigid that developers hate working with it because they cannot easily do what they need.

I still subscribe to the notion that objects with state should be relatively dumb. They should enforce that their state is kept "valid", or clearly expose an "erroneous" state, but otherwise they should rely on higher-level objects to manipulate them, show them, make their noises, etc. The object itself should just contain the data needed to know how to draw or make noise. It should be a higher level object that actually does the drawing.

I've read that composition is a good thing, and it probably is better than inheritance for its intended purpose, but I don't think either is necessarily required here. Try to imagine a data structure that could describe how to draw a special widget, like a checkbox. It doesn't do the drawing itself. It just describes how the widget should look. Maybe a collection of ordered drawing operations or something with parameters. Worst case, it would be pixels in 2D or 3D space, but that's probably overkill performance-wise.

You don't want to ever repeat yourself, and if objects are literally doing low-level drawing operations then you're repeating yourself. Describe them in data and then they become much more powerful because they can be used by more code. It becomes much easier to implement different backends because the shared code is literally just data, and you don't have to reimplement every widget. For example, perhaps that same data could be used to render a TUI using the same "widgets". Or perhaps it could be used to describe it to a blind user (that might be a bit extreme, but it's worth considering).

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

I have a vague idea of what you're trying to say, but I don't know what that would mean code wise.

For example, in my Skyline game, Missiles and Lasers and such are just data with a default draw method, but their actions and everything are dictated by the AI class for the player and Enemy. Basically they're just lists of data. Is this what you're talking about? I'm just not familiar with this kind of programming. It take MVC to a new Controller level I don't have much experience with.

ZoriaRPG
Member #16,714
July 2017
avatar

Honestly, I like to see simple building calls such as:

AddUIElement(type, x, y, *f(), ...properties...)

I see a lot of attempts to make UIs intuitive, that pan out to heavily objectified nightmares. The KISS method is best.

Menu building should also be as basic as possible. I see a lot of stuff in this department that fails, often.

AddMenu(TYPE, *f(), x, y, font, ...properties...)
AddToMenu(obj_type, "label", *f())

Likewise, CreateDialogue(TYPE, x, y, width, height, bool scrollbar)

In the above, *f/&f() would be a pointer to a function that is called when the element is selected. Obviously, radio buttons and the like will not often need this, so they'd be pointed to NULL frequently, but there may be points where you want to call a function, such as a radio button changing the visual appearance of the UI when selected, or making some objects hidden/unhidden.

Each of these should be typed to some kind of pointer, so that you can reference the base pointer of an object to the children thereof. Some kind of struct perhaps? :shrug:

Structure of this sort is what I like to see. Of course, I also like to see code output that isn't terribly unoptimised. :/

The UI also needs to refresh on a continual basis, but the refresh call should be supplied by the user, so that the user can determine the refresh rate interval in the loop.

I much prefer to do if ( f%10 ) RefreshUI(), than have the Ui refresh every clock cycle. -- I think that's best expressed as RefreshUI(10). --

The end result should be easy to use, and fast. After a few years of dealing with jwin, almost abything would be better, yet I find that the Ui bases that I've examined were just as cumbersome, and unintuitive.

Audric
Member #907
January 2001

ZoriaRPG said:

Honestly, I like to see simple building calls such as:
AddUIElement(type, x, y, *f(), ...properties...)

I generally agree, these libraries are quicker to learn, but note that this favors writing the code : Afterwards, when you read AddUIElement(ETextBox, 14, 5, 3, 2, 8), what is 2 ? A width in pixels, a maximum number of characters, a color ?

GUI systems which are more based on objects and properties do it as an attempt to make it easier to read the code, later. It still has drawbacks, such as being more verbose and the issue of choosing names : Does the API use Label.Text, Label.Content, or Label.TextContent ?

In Grafx2 I originally provided only "imperative" GUI functions, but I know the end-result is unreadable without checking the documentation.

OK, POINTS,RES,GRACEPERIOD, circle,square,lines,aa = inputbox("Voronoi Crystallize Filter",            
         "POINTS: 3-100.000",       POINTS, 3,100000,0,
         "Min Point Separation",    RES, 0,100,0,
         "Fuzzy Width (Lines)*",    GRACEPERIOD,  0,16,0,
         "1. Euclidean Distance",   1,  0,1,-1,
         "2. Maximum Distance",     0,  0,1,-1,
         "*Draw Fuzzy Lines",       0, 0,1,0,
         "(*)Interpolation/AA",     1, 0,1,0                   
);

I tried to provide a data-oriented model. You may need documentation to write code using it (manual or other scripts as examples) but when you read the code a year later it's rather self-explanatory :

#SelectExpand
1local counter = gui.label{x=10, y=54, value=0, format="% .3d"} 2local form = gui.dialog{ 3 title="Dialogtest", 4 w=100, 5 h=150, 6 // all unnamed parameters are widgets 7 gui.button{ label="+", 8 x=6, y=38, w=14, h=14, repeatable=true, click=function() 9 counter.value=counter.value+1; 10 end}, 11 gui.button{ label="-", 12 x=26, y=38, w=14, h=14, repeatable=true, click=function() 13 counter.value=counter.value-1; 14 end}, 15 gui.button{ label="Close", 16 x=6, y=18, w=54, h=14, key=27, click=function() 17 return true; // causes closing 18 end}, ...

Although I have to say I could never find a way to support dropdown lists, enabling user code to browse, rename, add and remove the items.

bamccaig
Member #7,536
July 2006
avatar

What I was saying is that instead of each widget having raw draw calls they instead maintain state of how they should be drawn.

#SelectExpand
1class IDrawingOperation { // Line, Circle, FilledRectangle, Bitmap, etc. 2... 3 virtual void draw(const & IPosition, const & IGraphicsDriver) const = 0; 4}; 5 6class IRenderable { 7... 8 virtual const std::list<IDrawingOperation > getDrawingInstructions(void) const = 0; 9} 10 11class Widget: public IRenderable { 12... 13 std::list<IDrawingOperation > drawingInstructions; 14 virtual const std::list<IDrawingOperation > getDrawingInstructions(void) const; 15}; 16 17const std::list<IDrawingOperation > Widget::getDrawingInstructions(void) const { 18 return this->drawingInstructions; 19} 20 21... 22 23void Renderer::draw(const IPosition & position, const IDrawingOperation & op) { 24 op.draw(position, this->graphicsDriver); 25} 26 27void Renderer::render(const IRenderable & renderable, const IPosition & position) { 28 for (auto & op : renderable.getDrawingInstructions()) { 29 this->draw(position, op); 30 } 31} 32 33... 34 35Renderer renderer(AllegroGraphicsDriver(display), ...); 36Textbox textbox = ...; 37 38renderer.render(textbox);

The code is all contrived and missing a lot of necessary bits (e.g., calculating the position based on a layout, parents, and the widget's own attributes, etc.). The core point I'm trying to make in this post is that the widget never does a drawing call itself. The drawing is handled at a higher-level. The widget stores what it currently looks like in terms of lines or pixels. A simple textbox might be a rectangle with thin black or grey lines, and possibly text drawn inside of it, and possibly an intermittent cursor shape. Over time, the calculated drawing instructions will vary as time passes and the state of the widget changes. A higher level system actually draws all the widgets using their set of drawing operations.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

C/C++ lack named parameters, but languages that support them (e.g., C#) have a baked in solution to that problem (Visual Studio intellisense is also incredibly useful for that).

One alternative solution is wrapping the different properties into custom types like size, position, flagx, etc., so you're never just passing primitives. That might add an insignificant overhead in some cases, but it's probably well worth the readability. It'll also add significant keystrokes and line length though.

On the subject of readability and terseness, an alternative solution would be describing the widget in a scripted language or something. It can be much more readable and more flexible than C++. There would obviously be a non-negligible overhead, but it's a GUI so it can probably afford it (most of the time a GUI is just spinning waiting for input anyway).

#SelectExpand
1factory.create( 2 R"|( 3 Window 4 font=Helvetica 5 size=400,400 6 children= 7 TwoColumnLayout 8 children= 9 Label 10 text=Username: 11 Textbox 12 name=password 13 secret=true 14 type=Textbox 15 width=50 16 Button 17 onclick=login_click 18 name=login 19 text=Login 20 )|" 21);

You can either bind native functions to to "scripted" names within the system to bind event handlers dynamically, or bind the event handlers directly after the widget is created.

The exact syntax of the scripting language is up in the air. Whatever you can come up with that is easy to read, write, and machine-readable.

Erin Maus
Member #7,537
July 2006
avatar

bamccaig said:

The exact syntax of the scripting language is up in the air. Whatever you can come up with that is easy to read, write, and machine-readable.

It's called Lua!

This:

#SelectExpand
1local WIDTH = 320 2local HEIGHT = 240 3 4W.Root { 5 W.Panel { 6 size = T { WIDTH, HEIGHT }, 7 }, 8 9 W.Label { 10 text = "Create map...", 11 position = T { 16, -32 }, 12 style = W.Style.Label { 13 font = "Resources/Renderers/Widget/Common/DefaultSansSerif/Bold.ttf", 14 fontSize = 32, 15 color = { 1, 1, 1, 1 }, 16 textShadow = true 17 } 18 }, 19 20 W.GridLayout { 21 padding = T { 16, 16 }, 22 uniformGridSize = T { true, WIDTH / 2 - 16 * 2, 32 }, 23 size = T { WIDTH, HEIGHT * (2 / 3) }, 24 25 W.Label { text = "Width:" }, 26 W.Button { text = "32" }, 27 W.Label { text = "Height:" }, 28 W.Button { text = "32" }, 29 W.Label { text = "Tile Set:" }, 30 W.Button { text = "GrassyPlain" }, 31 }, 32 33 W.GridLayout { 34 size = T { WIDTH, HEIGHT * (1 / 3) }, 35 position = T { 0, HEIGHT * (2 / 3) }, 36 37 W.Button { 38 text = "OK", 39 onClick = function(self) 40 self:createMap() 41 end 42 }, 43 44 W.Button { 45 text = "Cancel", 46 onClick = function(self) 47 self:close() 48 end 49 } 50 } 51}

Is nicer than this:

#SelectExpand
1local NewMapInterface = Class(Widget) 2NewMapInterface.WIDTH = 320 3NewMapInterface.HEIGHT = 240 4function NewMapInterface:new(application) 5 Widget.new(self) 6 7 self.application = application 8 9 local width, height = love.window.getMode() 10 self:setPosition( 11 width / 2 - NewMapInterface.WIDTH / 2, 12 height / 2 - NewMapInterface.HEIGHT / 2) 13 self:setSize(NewMapInterface.WIDTH, NewMapInterface.HEIGHT) 14 15 local panel = Panel() 16 panel:setSize(self:getSize()) 17 self:addChild(panel) 18 19 local titleLabel = Label() 20 titleLabel:setText("Create map...") 21 titleLabel:setStyle(LabelStyle({ 22 font = "Resources/Renderers/Widget/Common/DefaultSansSerif/Bold.ttf", 23 fontSize = 32, 24 color = { 1, 1, 1, 1 }, 25 textShadow = true 26 }, application:getUIView():getResources())) 27 titleLabel:setPosition(16, -32) 28 self:addChild(titleLabel) 29 30 local inputsGridLayout = GridLayout() 31 inputsGridLayout:setPadding(16, 16) 32 inputsGridLayout:setUniformSize(true, NewMapInterface.WIDTH / 2 - 16 * 2, 32) 33 inputsGridLayout:setSize(NewMapInterface.WIDTH, NewMapInterface.HEIGHT * (2 / 3)) 34 35 self:addChild(inputsGridLayout) 36 37 local widthLabel = Label() 38 widthLabel:setText("Width:") 39 inputsGridLayout:addChild(widthLabel) 40 41 self.widthInput = TextInput() 42 self.widthInput:setText("32") 43 inputsGridLayout:addChild(self.widthInput) 44 45 local heightLabel = Label() 46 heightLabel:setText("Height:") 47 inputsGridLayout:addChild(heightLabel) 48 49 self.heightInput = TextInput() 50 self.heightInput:setText("32") 51 inputsGridLayout:addChild(self.heightInput) 52 53 local tileSetLabel = Label() 54 tileSetLabel:setText("Tile Set:") 55 inputsGridLayout:addChild(tileSetLabel) 56 57 self.tileSetIDInput = TextInput() 58 self.tileSetIDInput:setText("GrassyPlain") 59 inputsGridLayout:addChild(self.tileSetIDInput) 60 61 local buttonsGridLayout = GridLayout() 62 buttonsGridLayout:setPadding(16, 0) 63 buttonsGridLayout:setUniformSize(true, NewMapInterface.WIDTH / 2 - 16 * 2, 32) 64 buttonsGridLayout:setSize(NewMapInterface.WIDTH, NewMapInterface.HEIGHT * (1 / 3)) 65 buttonsGridLayout:setPosition(0, NewMapInterface.HEIGHT * (2 / 3)) 66 self:addChild(buttonsGridLayout) 67 68 self.okButton = Button() 69 self.okButton.onClick:register(function() 70 self:createMap() 71 end) 72 self.okButton:setText("OK") 73 buttonsGridLayout:addChild(self.okButton) 74 75 self.cancelButton = Button() 76 self.cancelButton.onClick:register(function() 77 self:close() 78 end) 79 self.cancelButton:setText("Cancel") 80 buttonsGridLayout:addChild(self.cancelButton) 81end

Which both result in this:

{"name":"611593","src":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/d\/1\/d12da1726756d150c2fe7bd1a68f10d5.png","w":802,"h":632,"tn":"\/\/djungxnpq2nug.cloudfront.net\/image\/cache\/d\/1\/d12da1726756d150c2fe7bd1a68f10d5"}611593

---
ItsyRealm, a quirky 2D/3D RPG where you fight, skill, and explore in a medieval world with horrors unimaginable.
they / she

Edgar Reynaldo
Major Reynaldo
May 2007
avatar

Bump for time to reply.

EDIT
So here's the basic design I came up with for the new WIDGETBASE class (unnamed at this point). I made a new branch called core on github and I am doing all my refactoring there.

Here's the new WIDGETBASE header : https://github.com/EdgarReynaldo/EagleGUI/blob/core/include/Eagle/Gui2/WidgetBase2.hpp

Source : https://github.com/EdgarReynaldo/EagleGUI/blob/core/src/Gui2/WidgetBase2.cpp

The new attribute system is stellar. Widgets inherit attributes first locally, then from their parent, then from their layout, then from their widget handler, then from the global attribute pool. This makes skinning the GUI super easy.

New and improved interface, with a much cleaner API. Major features are now accessible only via setters, with virtual callbacks. No more interface duplication BS. It only leads to a proliferation of redundant code.

EDIT
However, now I have to decide what all the properties will be called. For instance, I have several different border drawing functions. Do I store them under "BorderFillType"? Or something else? How do I make my naming consistent and registered, listed and published for a standard?

Go to: