The Beginners Guide to Falagard skinning - Part I
Written for CEGUI 0.4
Works with versions 0.4.x (obsolete)
The Beginners Guide to Falagard series
Introduction
The Falagard system in CEGUI 0.4 and up is a powerful tool. By using it you can make your GUI elements look like you want and not how some coder thought looked cool. Use Falagard to your advantage and you can do alot of nice things from XML that you might think needed real code.
Falagard allows you not only to define how the rendering of a window proceeds, you can create new representational features as well by creating new properties for the window and using them in your imagery configurations.
Before I go too far glorifying Falagard, we'll just jump right into it :-)
Basics
Falagard is a part of the CEGUI core and is implemented in C++. It is completely possible to write a skin in C++, but it's is not very practical. You have to recompile every time you change something. Fortunately CEGUI supports creating a Falagard skin from a custom XML format.
This Falagard XML format is what we're going to be looking at throughout this tutorial. As I think learning by doing is the way to go, we'll try to produce something useful with this tutorial - a fully working look'n'feel for a regular button.
The beginning:
<?xml version="1.0" ?> <Falagard> <WidgetLook name="MyButton"> </WidgetLook> </Falagard>
<?xml version="1.0" ?> <Falagard version="7"> <WidgetLook name="MyButton"> </WidgetLook> </Falagard>
This is the truely minimal XML that defines a widget look. A widget look contains all the information needed to render a specific type of window, a widget look is also what we would define as a look'n'feel. The first line tells the parser that this is really a XML document. This must always be the first line of a Falagard XML skin. The second line opens the Falagard tag. This is always the first thing we do after the <?xml ... ?> tag. I hope you're somewhat familiar with XML or at least HTML as this tutorial might be a little difficult to understand if you're not. Now you're warned. The third line opens a WidgetLook tag. This is where we cram all the details about our look'n'feel. The fourth line closes the WidgetLook tag. We must always remember to close our tags. The fifth line closes the Falagard tag. Again - always remember to close your tags.
While this simple XML snippet is good for showing you how to start out, it is completely useless as it does nothing.
To make our look'n'feel interesting we need to tell it what to do. So let's have a look at the Falagard data structure:
- WidgetLook
- Property definitions
- Properties
- Named areas
- Child components
- Imagery sections
- State imagery
This might be overwhelming. It might not. Either way it's the how it is. Falagard is a complex system... But it's really not that hard once you get down the basics.
The look'n'feel we are going to produce is for a Falagard/Button type window. This window type is simple, and we don't have to know about all of the parts of a look'n'feel. We only need to know what we'll be using. Furthermore we'll keep it very simple which leaves us at a point where we can make something work with very little.
I'm still going to give a short description of each item in the list though:
- WidgetLook - This is where we add all the other components for our look'n'feel. Consider it the look'n'feel.
- Property definitions - A property definition is the creation of a new property in the window that gets the look'n'feel assigned.
- Properties - A property in CEGUI is a window variable that we can access from anywhere. Even outside C++ (think XML window layouts). We often assign default values to properties from a look'n'feel.
- Named areas - A named area defines a rectangle, inside the window we are skinning, with a distinct name. Often used to define where to draw some specific piece of imagery that needs special handling by CEGUI as thus cannot be completely defined in the look'n'feel.
- Child components - Some window types need child windows. This could be scrollbar, a titlebar etc. We specify what type of window this child will be customize it for the situation from the look'n'feel.
- Imagery sections - An imagery section is a collection of imagery components tagged with a name. We use imagery sections to split the renderable parts of our look'n'feel into sections which can be drawn independently. Like for example having the frame in one section and the background in another.
- State imagery - Tells CEGUI what to draw when. In a state imagery we draw all the different imagery sections we need for a specific window "situation". For our button we will have: Normal, Hover, Pushed, Disabled.
Ok. I hope this clarifies it a bit. We'll go into more detil about the different parts as we need them.
Requirements
Every time you write a look'n'feel you need to make find out what the requirements are for the type of window you want to skin. To get this information you either look at the header files in the Falagard module source directory, or you reference the manual. This is available both online and in the documentation directory in the SDK as a PDF.
The online version is available on the page Falagard skinning system for CEGUI. For the list of these requirements I'm talking about the Falagard Window Renderer Requirements is what you're looking for. If we look in there we'll see that Falagard/Button only require 1 StateImagery definition to be present. Normal.
It allows up to four StateImagery definitions to cover all its possible states: Normal, Hover, Pushed and Disabled.
We'll start out the easy way and just make the Normal state ;-) So now our XML looks like this:
<?xml version="1.0" ?> <Falagard> <WidgetLook name="MyButton"> <b><StateImagery name="Normal"> </StateImagery></b> </WidgetLook> </Falagard>
If you compare to the last XML piece, the only difference is the addition of a StateImagery tag with its name attribute set to "Normal". This makes sure that we live up to the requirement of a "Normal" state definition. As I said earlier the WidgetLook is the look'n'feel so it is - of course - inside it we add the StateImagery tag.
This still extremely simple look'n'feel is actually a fully valid look'n'feel. It implements what is required by the manual. Though as you might have guessed it still does nothing. To get it to render anything we have to define the imagery sections mentioned earlier.
Imagery
For our look'n'feel we'll just want a single image to be stretched over the entire window area. That is each corner from the image mapped to each corner of the window.
To acheive this the first thing we need to do is to add a ImagerySection to our look'n'feel.
<?xml version="1.0" ?> <Falagard> <WidgetLook name="MyButton"> <b><ImagerySection name="bg"> </ImagerySection></b> <StateImagery name="Normal"> <b><Layer> <Section section="bg" /> </Layer></b> </StateImagery> </WidgetLook> </Falagard>
Now we have added the imagery section, and named it "bg". This name is just something we choose.
Inside the state imagery for the "Normal" state a layer has been added. The Layer tag allow you to control the z-ordering of layers inside in a StateImagery, but we only need one layer for this look'n'feel so let's not worry too much about that.
In the layer a Section has been added. This tag tells the layer its in that we want the ImagerySection named "bg" to be rendered as part of this layer.
This is nice. The look'n'feel is all ready and hooked up to render the imagery section "bg" as the imagery for the "Normal" state. But our imagery section is still empty, and this means our look'n'feel really is no better than the previous. It still does nothing (at least visually).
Now that we're over the really basic and boring stuff, let's make it draw that image...
To draw a single image we use the tag ImageryComponent. This tag requires us to define the area in which we want the image to be drawn, as well as the image to draw.
The area is the most complex part of practically all look'n'feels. It is designed to be flexible and uses an extended version of the unified coordinate system of CEGUI 0.4 and up. Besides the regular two numbers containing the relative and absolute component of the coordinate or size Falagard also allows us to use the dimensions of images, windows, text and even properties. We can also perform arithmetic operations on these dimensions to a certain extent.
Any area needs to define 4 dimensions to fully describe the area. There is some flexibility in how you define the dimensions though. Probably the most common is to use one of "LeftEdge", "TopEdge", "RightEdge" or "BottomEdge". These map to edges of the window that recieves the look'n'feel, but you can specify "Width" and "Height" instead of "RightEdge" and "BottomEdge".
The look'n'feel we're creating only needs a simple area that covers the entire widget. We'll take the XML required for that in two steps.
<Area> <Dim type="LeftEdge"> </Dim> <Dim type="TopEdge"> </Dim> <Dim type="RightEdge"> </Dim> <Dim type="BottomEdge"> </Dim> </Area>
The area itself is created with the Area tag. Inside it we have the 4 dimensions. We must explicitly state the type of dimension for each one. The order is important:
- LeftEdge
- TopEdge
- RightEdge / Width
- BottomEdge / Height
You don't have to use both width and height. For example using "RightEdge" and "Height" is fine.
This tells the area how to interpret the dimensions that we are now going to add:
<Area> <Dim type="LeftEdge"> '''<AbsoluteDim value="0" />''' </Dim> <Dim type="TopEdge"> '''<AbsoluteDim value="0" />''' </Dim> <Dim type="RightEdge"> '''<UnifiedDim scale="1" offset="0" type="RightEdge" />''' </Dim> <Dim type="BottomEdge"> '''<UnifiedDim scale="1" offset="0" type="BottomEdge" />''' </Dim> </Area>
Inside each Dim tag we have added a dimension. Let's take them one at a time.
- <AbsoluteDim value="0" /> - This tell it to put our "LeftEdge" Dim at pixel position 0. All dimensions are always relative to window that has the look'n'feel, so 0 will in this case be the left edge of our widget as we planned.
- <AbsoluteDim value="0" /> - Just the same as before. But this time we're in a "TopEdge" Dim so it will result in the top edge of our window.
- <UnifiedDim scale="1" offset="0" type="RightEdge" /> - A unified dimension. This one has a scale (relative) component of exactly one. And a offset (absolute) value of zero. As type we've set "RightEdge". This is what the scale component get multiplied by to get some thing useful (pixel values). One times RightEdge will equal RightEdge, which is exactly the window dimension we want.
- <UnifiedDim scale="1" offset="0" type="BottomEdge" /> - Same as before but the bottom edge of our window as the dimension.
Now the area to span our entire is worked out, let's add that ImageryComponent and give it its area.
<?xml version="1.0" ?> <Falagard> <WidgetLook name="MyButton"> <ImagerySection name="bg"> <b><ImageryComponent> <Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="RightEdge"> <UnifiedDim scale="1" offset="0" type="RightEdge" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area> </ImageryComponent></b> </ImagerySection> <StateImagery name="Normal"> <Layer> <Section section="bg" /> </Layer> </StateImagery> </WidgetLook> </Falagard>
Now the area is in place. The ImageryComponent tag has been added to our ImagerySection and inside it we have out the area we just created. All thats missing now is the image!
We'll assume we have a imageset loaded called "MyImages". And inside this imageset we have the image "ButtonBG" defined. This is the image we'll use for our button.
The XML looks like this
<Image imageset="MyImages" image="ButtonBG" />
Simple eh?
We also wanted the image to be stretched across the whole area of the ImageryComponent. The default formatting in top/left alignment. Not good enough...
We add the formatting tags
<VertFormat type="Stretched" /> <HorzFormat type="Stretched" />
Vertical format must come first. Both sets stretching as the formatting. Which is what we need. That's all we wanted in the look'n'feel so let's have a look at the XML for this extremely simple button skin.
<?xml version="1.0" ?> <Falagard> <WidgetLook name="MyButton"> <ImagerySection name="bg"> <ImageryComponent> <Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="RightEdge"> <UnifiedDim scale="1" offset="0" type="RightEdge" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area> <b><Image imageset="MyImages" image="ButtonBG" /> <VertFormat type="Stretched" /> <HorzFormat type="Stretched" /></b> </ImageryComponent> </ImagerySection> <StateImagery name="Normal"> <Layer> <Section section="bg" /> </Layer> </StateImagery> </WidgetLook> </Falagard>
The three new tags just got added after the area in the ImageryComponent, and we're done :)
Label
You might have noticed that I have'nt talked about the label until now. We'll need to take a look at it now or we won't get any text on our button. With Falagard you have full control of how the text is rendered.
For our look'n'feel we want the text to be centered both vertically and horizontally in the widget. We also want the text to wrap (word wrap) if it's too long to render as one line without being clipped.
We'll make a new ImagerySection for our label. This gives us a nice seperation of parts to render. "bg" being the background, and a new section called "label" to represent the - You guessed it - label. Inside this new ImagerySection we add a TextComponent which we'll set up to render the text like we want.
<ImagerySection name="label"> <TextComponent> </TextComponent> </ImagerySection>
Like a ImageryComponent a TextComponent must have an area. For our button we'll allow text to span the entire window. This means that we can reuse the Area from before.
A TextComponent by default renders the text of the window that has the look'n'feel assigned. This is exactly what we want for our button.
The formatting we wanted is easily made by using the same tag we used before (to format the background image) but with another type. The formattings supported are different for TextComponent and ImageryComponent.
Let's look at the XML for this new ImagerySection "label":
<ImagerySection name="label"> <TextComponent> <b><Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="RightEdge"> <UnifiedDim scale="1" offset="0" type="RightEdge" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area> <VertFormat type="CentreAligned" /> <HorzFormat type="WordWrapCentreAligned" /></b> </TextComponent> </ImagerySection>
We added the same area as in the "bg" ImagerySection, and added the formattings we wanted. Take a look at Falagard XML Enumeration Reference for the complete listing of supported formattings (and lots of other stuff).
This ImagerySection is done. It will render the label with the correct formatting using a white colour. By not specifying a colour we always choose white, which is useful as we can modulate it into any colour we choose later on. Which we of course will :-)
To actually use this new "label" section we must add it to our state imagery like we did with the "bg" section earlier. So here's what the XML looks like so far:
<?xml version="1.0" ?> <Falagard> <WidgetLook name="MyButton"> <b><ImagerySection name="label"> <TextComponent> <Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="RightEdge"> <UnifiedDim scale="1" offset="0" type="RightEdge" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area> <VertFormat type="CentreAligned" /> <HorzFormat type="WordWrapCentreAligned" /> </TextComponent> </ImagerySection></b> <ImagerySection name="bg"> <ImageryComponent> <Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="RightEdge"> <UnifiedDim scale="1" offset="0" type="RightEdge" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area> <Image imageset="MyImages" image="ButtonBG" /> <VertFormat type="Stretched" /> <HorzFormat type="Stretched" /> </ImageryComponent> </ImagerySection> <StateImagery name="Normal"> <Layer> <Section section="bg" /> '''<Section section="label" />''' </Layer> </StateImagery> </WidgetLook> </Falagard>
This look'n'feel adds the fully working label (still white) to our button skin. This is starting to become a useful look'n'feel, so let's give it the final touch by adding handling of the three other states Hover, Pushed and Disabled that we left out before.
Colours
We want to use different text colours for each of these states. We also want to able to customize those colours on a per widget basis. This is made possible by the property definition.
A PropertyDefinition creates a new property in the window that gets the look'n'feel assigned. The property we create is a simple string value, but we choose what it's used for. Not CEGUI.
In total we have 4 states for our button counting the new ones, so we'll add 4 new properties to store the text colour for each state. The XML is like so:
<PropertyDefinition name="NormalTextColour" initialValue="FFFFFFFF" redrawOnWrite="true" /> <PropertyDefinition name="HoverTextColour" initialValue="FFFF0000" redrawOnWrite="true" /> <PropertyDefinition name="PushedTextColour" initialValue="FF00FF00" redrawOnWrite="true" /> <PropertyDefinition name="DisabledTextColour" initialValue="7F888888" redrawOnWrite="true" />
- name is the name the new property will be available under.
- initialValue is the default value for the property.
- redrawOnWrite when set to true the window will be redrawn when the value of the property changes.
These properties will be available from both C++ code and in XML window layouts. So choosing a good name is a good idea. You can create properties that you don't use for anything inside the look'n'feel. These will still become available which can be a good way of creating new variables for windows that you might need in your application.
The default values represent colours in the format CEGUI would expect a colour for a built-in property. Colours for CEGUI are always written in hexadecimal.
The format is: "AARRGGBB" where:
- AA = Alpha
- RR = Red
- GG = Green
- BB = Blue
This gives our properties the values of
- white for Normal
- red for Hover
- green for Pushed
- 50% tranparent grey for Disabled
Setting a new value for any of these properties will make sure the button is re-rendered to show the new colour.
The only places in the look'n'feel that know which state we are in are the StateImagery tags. The Section tags inside these allow for colours to be specified. The XML for the label in the Normal state looks like this:
<Section name="label"> <ColourProperty name="NormalTextColour" /> </Section>
The ColourProperty tag inside our Section ensures that all imagery in the imagery section "label" has its colour modulated by the value of the property "NormalTextColour". The imagery section "label" render only white text, so by doing this we ensure that the colour rendered is exactly that of the value of the property "NormalTextColour".
We now know what to do so lets look at the XML so far for this look'n'feel:
<?xml version="1.0" ?> <Falagard> <WidgetLook name="MyButton"> <b><PropertyDefinition name="NormalTextColour" initialValue="FFFFFFFF" redrawOnWrite="true" /> <PropertyDefinition name="HoverTextColour" initialValue="FFFF0000" redrawOnWrite="true" /> <PropertyDefinition name="PushedTextColour" initialValue="FF00FF00" redrawOnWrite="true" /> <PropertyDefinition name="DisabledTextColour" initialValue="7F888888" redrawOnWrite="true" /></b> <ImagerySection name="label"> <TextComponent> <Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="RightEdge"> <UnifiedDim scale="1" offset="0" type="RightEdge" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area> <VertFormat type="CentreAligned" /> <HorzFormat type="WordWrapCentreAligned" /> </TextComponent> </ImagerySection> <ImagerySection name="bg"> <ImageryComponent> <Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="RightEdge"> <UnifiedDim scale="1" offset="0" type="RightEdge" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area> <Image imageset="MyImages" image="ButtonBG" /> <VertFormat type="Stretched" /> <HorzFormat type="Stretched" /> </ImageryComponent> </ImagerySection> <StateImagery name="Normal"> <Layer> <Section section="bg" /> <b><Section section="label"> <ColourProperty name="NormalTextColour" /> </Section></b> </Layer> </StateImagery> <b><StateImagery name="Hover"> <Layer> <Section section="bg" /> <Section section="label"> <ColourProperty name="HoverTextColour" /> </Section> </Layer> </StateImagery> <StateImagery name="Pushed"> <Layer> <Section section="bg" /> <Section section="label"> <ColourProperty name="PushedTextColour" /> </Section> </Layer> </StateImagery> <StateImagery name="Disabled"> <Layer> <Section section="bg" /> <Section section="label"> <ColourProperty name="DisabledTextColour" /> </Section> </Layer> </StateImagery></b> </WidgetLook> </Falagard>
Not too hard was it? The look'n'feel reached a whole new level with these changes, as state is reflected in the text colour.
To sum up what we've made:
- A look'n'feel for the Falagard/Button window type.
- Static background image.
- Formatted label.
- Label colour dependent on current state.
That will be enough for this time. It's getting early ;-)
I hope this tutorial is useful and that it will help expose Falagard more broadly.
Please give feedback in the discussion page so I can improve the tutorial where needed.
--User:lindquist 06:10, 6 Dec 2005 (CET)