[Solved] Need advice on Skinning

kungfoomasta

04-06-2008 23:50:44

I'm starting on the new skinning system, coming up with various approaches, but so far I haven't come up with anything that feels like a good solution. It is most important that this new system be easy to work with, so that people can easily define how their widgets look.

I have a concept of a Widget "class", which refers to the functionality of the widget. Examples would be "Button", "Label", "TextBox", "ScrollBar", etc. For each class of widget, I want to support multiple types. A "Type" is a defined appearance for a widget. Examples of "Button" types would be "ScrollUp1", "Default", "Square", "Circular", etc.

Also I have the idea of a WidgetSkinDefinition. Every widget has a specific set of skin elements it uses to draw itself. Every Button should have the same number of elements. Likewise, a ScrollBar would have a different set of elements. You should not be able to have 2 buttons that have a different number of elements. For this reason I wanted to create a WidgetSkinDefinition, so that all widget types must follow the same interface.

For example, right now in my Button::onDraw method, I make this call:


brush->setTexture("qgui.button.png");
brush->drawRectangle(Rect(mTexturePosition,mWidgetDesc->dimensions.size),UVRect(0,0,1,1));


See how I've hardcoded the "qgui.button.png" texture? I need to have some sort of alias that can be used, so that users can use different images.

So I'm thinking the WidgetSkinDefinition should define the aliases, for example "default","down","over", and the WidgetType should define the textures, or UVCoords, of each alias. So how should I implement the WidgetSkinDefinition? Should I parse a text file that outlines the aliases, and hope that it matches the string aliases used in the code? Should the Button class define the definition, and just read Type definitions from file? If I embed the definition in the Button class, every instance will be executing the same redundant code. Is there an elegant way to define the skinning interface once, without having to create an instance of a Button?

Any alternative approaches are welcome.

Squirell

05-06-2008 01:08:02

I'm a bit confused here but I'll still take a stab at it. How do you plan on creating a button once the system is done. Something like this?


createButton(skinType)
createButton("default"); // More concrete example


If so, why not create a text file resource similar to the following. I made the syntax like a material script, but it could obviously be anything.


button // All the button "types" are contained here
{
default // A type you can specify
{
texture qgui.button.png
uv 0 0 1 1
}
pretty // Another type
{
...
}
// You can specify types to your heart's content
}
scrollbar
{
...
}
// Keeps going for all the widgets


Then when you make the createButton("default") call. (Or whatever you want to make it). The button widget class looks in the aliases file for the button subsection and the default type within that section. If it finds it, it uses that information, if not some sort of error.

Edit: I should clarify that it wouldn't do that every time it renders like I may have made it sound, but performs the lookup at initialization time.

Zini

05-06-2008 09:46:20


So how should I implement the WidgetSkinDefinition? Should I parse a text file that outlines the aliases, and hope that it matches the string aliases used in the code? Should the Button class define the definition, and just read Type definitions from file? If I embed the definition in the Button class, every instance will be executing the same redundant code. Is there an elegant way to define the skinning interface once, without having to create an instance of a Button?


Sure. You have the QuickGUI::Root class (I hope you still have this class in the new version; with all this interface changes one never knows for sure ;) ). Create a central repository of skin definitions in QuickGUI::Root and just add a pointer to the individual widget.

kungfoomasta

05-06-2008 17:32:23

@Squirell:

This is how you would create a button:

QuickGUI::ButtonDesc d;
d.type = "default";
...
mGUIManager->getActiveSheet()->createButton(d);


What code do you use to write/read the material like syntax? Maybe I should check out the Material code, the ScriptParser I'm using is very simple and doesn't handle nested braces well.

@Zini:

I think what I will do is create a static method for each widget and have it create and register its skin definition. And for the default widges I create I'll have the root register their definition like you say. Users will have to register everything on their own, but I'll put up a wiki sometime to document how to create a custom widget.

I'll start with this direction and see where it takes me. :)

Squirell

05-06-2008 17:44:32

I would definitely look at at the material script parser. Last time I looked it was very clear and made things pretty easy. If you need more help with that I'd be happy to help.

I like the idea of having QuickGUI::Root handle loading the skin script. I would inherit it from Ogre::ScriptLoader, define the couple of functions you have to, and let the normal ogre resource system handle it from there.

kungfoomasta

05-06-2008 19:16:45

I took a peek at the OgreMaterialScriptCompiler.h and OgreMaterialSerializer.h, and I think they don't really fit what I need. I need simple classes that can read and write text in a format similar to material scripts.

For example lets say I want this script:


WidgetSkinType Button
{
Type default
{
SkinElement default
{
uvcoord 0 0 0.25 0.25
top_border 5
bottom_border 5
left_border 5
right_border 5
}
SkinElement over
{
uvcoord 0.25 0 0.25 0.25
top_border 5
bottom_border 5
left_border 5
right_border 5
}
SkinElement down
{
uvcoord 0.5 0 0.25 0.25
top_border 5
bottom_border 5
left_border 5
right_border 5
}
}

Type scrollLeft1
{
...
}
}

WidgetSkinType HScrollBar
{
Type default
{
SkinElement background
{
uvcoord 0.75 0 0.25 0.25
top_border 5
bottom_border 5
left_border 5
right_border 5
}

Component scrollLeft
{
Class Button
Type scrollLeft1
}

...
}
}


The reader should read in the { to } as a "Definition". Definitions can have nested Definitions.

Maybe something with an interface like the following:

Definition* Reader::getDefinition(const Ogre::String& type, const Ogre::String& id);
Definition* Definition::getDefinition(const Ogre::String& type, const Ogre::String& id);
Ogre::StringVector Definition::getValues(const Ogre::String& id);


Maybe I'll look into rewriting how I read/write to file. If you want to help out with this it would be welcome. :)

Squirell

05-06-2008 21:30:01

I just took a look at them too and I think the new material script compiler system changes things so that it doesn't fit your needs. I would use Ogre::ScriptLoader like you do right now for the config files.

Fleshing out my earlier example, make QuickGUI::Root inherit from Ogre::ScriptLoader. That way when the root is initialized the script will be loaded.


namespace QuickGUI
{
// I don't make it a singleton and exclude a bunch of other things but the example still holds
class Root: public Ogre::ScriptLoader
{
public:
Root()
{
Ogre::ResourceGroupManager::getSingleton()._registerScriptLoader(this);
}

// Parse the script here
void parseScript(Ogre::DataStreamPtr &stream, const Ogre::String &groupName);

// etc etc etc
}
}


Also I would take a look at the OgreOverlayManager header and source. That was the example I followed when I was writing my version.

Finally, I would love to help you out. I'm happy to write code or just throw out ideas here, whichever you would prefer.

kungfoomasta

05-06-2008 22:10:08

Actually I want to keep the ScriptParser class, just update it to be able to read and give access to needed parts of the script data. In order to make use of Ogres ScriptParser functionality, you have to create it and register it early on in the initialization of Ogre. I don't want to force the Root to be created at this stage, in fact this might cause problems, since the root performs other operations, like initiliazing the SkinSetManager, which will get TexturePtr to all the Skin Images. (Can't get TexturePtr before initialization)

So basically the user would do the following:

- call a static function that creates and registers the QuickGUI::ScriptParser class
- create Root at some point in time

I'll also create a ScriptWriter class.

If you already have your own parser implemented that might speed things up to see it. I don't think it will be too difficult either way.

kungfoomasta

10-06-2008 22:18:49

After some more work on Skinning, I realized a potentially good idea. I wanted to share it to see if anybody agrees or disagrees.

Previously we had a SkinSet, which was a TextureAtlas that provided uv coords and a texture for drawing widgets. The main reason for the atlas was to minimize batching, by having one large texture represent several small ones. It also added the concept of a Skin, which contains the information describing how widgets appear. This didn't work out so well however, since it was made obvious that widgets will need several appearances.

Now that the render system has changed so that all widgets write to their window's texture (texture caching), we don't have to worry about minimizing batching. So whats the main reasoning for keeping the TextureAtlas useage? I'm thinking its more beneficial to remove the idea of a SkinSet.

Earlier in this thread I mentioned Widget Types and how that defines a widget's appearance. My idea is to require only a list of WidgetSkinTypes. What this means is that users will create and maintain a text file that defines the various types they use. Sharing types will be as easy as adding some text to a file and copying a texture to a resource directory. Widgets will not have a "skin" property, only a "WidgetSkinTypeName" property.

Earlier I posted what a SkinElement would look like using an atlas:

SkinElement default
{
uvcoord 0 0 0.25 0.25
top_border 5
bottom_border 5
left_border 5
right_border 5
}


Without an atlas it would look like:

SkinElement default
{
texture qgui.button.png
top_border 5
bottom_border 5
left_border 5
right_border 5
}


Skinning would result to maintenance of a text file named however you want, ie "PetWars.SkinTypes", and the textures that are referenced in the SkinElement definitions.

Squirell

11-06-2008 21:01:50

It makes sense to me. Give it a try and see how it goes I guess.

kungfoomasta

12-06-2008 20:20:53

I'm getting close to done! Here is a script I created for the Button WidgetSkinType:

Button.skinTypes

WidgetSkinClass Button
{
WidgetSkinType default
{
Child ComponentTypes
{
}

Child SkinElements
{
SkinElement default
{
Border_Bottom 2
Border_Left 2
Border_Right 2
Border_Top 2
Texture qgui.button.png
TileBackground false
TileBorders true
}

SkinElement down
{
Border_Bottom 2
Border_Left 2
Border_Right 2
Border_Top 2
Texture qgui.button.down.png
TileBackground false
TileBorders true
}

SkinElement over
{
Border_Bottom 2
Border_Left 2
Border_Right 2
Border_Top 2
Texture qgui.button.over.png
TileBackground false
TileBorders true
}

}

}

}


So if you had this defined, and it matched the Button's skin definition (It will throw exception if the type interface is wrong), you should be able to do something like this:

myButton->setWidgetSkinType("default");

Next step is updating the draw functions to use the skin system, now that its in place. :D

kungfoomasta

13-06-2008 22:23:42

I'm happy to say that the skinning system is complete. :D

Its really awesome to be able to add/edit/change skins all in a text file. I added in the ability to have empty SkinElements. For example, in my Sheet.skinTypes file, I made the following script:


WidgetSkinClass Sheet
{
WidgetSkinType default
{
Child ComponentTypes
{
}

Child SkinElements
{
SkinElement background
{
}

}

}

}


So a "default" Sheet will not be drawn, unless I add in the required fields into the SkinElement definition. Using this feature you can have invisible windows, or any part of any widget can be transparent if you wanted. And you don't have to use "default", you could make a more intuitive type like "transparent". Of course, QuickGUI will probably assume there is a "default" type for every widget. (Is this isn't the case, you will need to set the SkinType field of the Desc object before creating the widget)

I've also decided to rename "WidgetSkinType" to "SkinType", it was starting to annoy me with the long name.