The Beginners Guide to Falagard skinning - Part II
Written for CEGUI 0.4
Works with versions 0.4.x (obsolete)
The Beginners Guide to Falagard series
Introduction
Last time we looked at the essential basics to get something running with Falagard. This is great, but a simplistic look'n'feel for a button is a bit boring. So lets step forward and learn something new.
In this tutorial we will create a look'n'feel for the Editbox window type. This widget is interactive and uses new parts of the Falagard system, making it an excellent target for another tutorial.
So let's get started...
Requirements
We'll start out by looking at the requirements for the Editbox window type. Like always, the information we're looking for can be found in the Falagard Window Renderer Requirements.
In there we'll see that the Editbox is a bit more complex than the Button. But fear not - we'll get through every part of it ;-)
Let's start out with the StateImagery needed for this widget:
- Enabled
- Disabled
- ReadOnly
- ActiveSelection
- InactiveSelection
Unlike the Button we don't have any optional states. Enabled and Disabled should speak for themselves. ReadOnly is used when the Editbox is in read-only mode (easy huh?).
The ActiveSelection state is somewhat special. It should define what imagery to render for the selection graphics itself when the window is in an activated state (has focus). InactiveSelection is the same, except it is used when the window is not active (does'nt have focus).
Besides the StateImagery needed, a Editbox also requires a NamedArea called TextArea and a ImagerySection named Carat (yes, carat). We'll have a much closer look at these later on.
The initial XML (with the parts we know about) looks like this:
<?xml version="1.0" ?> <Falagard> <ImagerySection name="Carat"> </ImagerySection> <StateImagery name="Enabled"> </StateImagery> <StateImagery name="Disabled"> </StateImagery> <StateImagery name="ReadOnly"> </StateImagery> <StateImagery name="ActiveSelection"> </StateImagery> <StateImagery name="InactiveSelection"> </StateImagery> </Falagard>
We'll fill it out as we go...
Imagery
This look'n'feel will use a frame consisting of 8 different images from our favorite imageset MyImages. Furthermore we'll have a single image stretched to fill the hole in this frame. The images will have names that are easy to interpret:
TL | T | TR |
L | Bg | R |
BL | B | BR |
Each prefixed with Editbox. Forming imagenames like EditboxTL etc.
The FrameComponent tag may only occur inside an ImagerySection. Just like the ImageryComponent and TextComponent that we used in part I.
Using the FrameComponent is very much like using the ImageryComponent all the tags allowed are the same, but a FrameComponent allows up to nine images to be specified. It's purpose is to make making frames easy :-) A FrameComponent is special in the way it handles formatting compared to ImageryComponent though. Any formatting options are only applied to the background image - if it's specified. Yes. We don't have to set all nine of them. We can just choose the image "positions" we need. In our case we'll use all of them tough...
Let's look at the XML for this FrameComponent:
<FrameComponent> <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 type="TopLeftCorner" imageset="MyImages" image="EditboxTL" /> <Image type="TopEdge" imageset="MyImages" image="EditboxT" /> <Image type="TopRightCorner" imageset="MyImages" image="EditboxTR" /> <Image type="RightEdge" imageset="MyImages" image="EditboxR" /> <Image type="BottomRightCorner" imageset="MyImages" image="EditboxBR" /> <Image type="BottomEdge" imageset="MyImages" image="EditboxB" /> <Image type="BottomLeftCorner" imageset="MyImages" image="EditboxBL" /> <Image type="LeftEdge" imageset="MyImages" image="EditboxL" /> <Image type="Background" imageset="MyImages" image="EditboxBg" /> <VertFormat type="Stretched" /> <HorzFormat type="Stretched" /> </FrameComponent>
You should recognise the area. It's the full area of the window, exactly like the areas we used in part I. Reason: We want the frame and background to cover the entire window. Simple :-)
Nine images are specifed. The order does not matter. But note that compared to the ImageryComponent used last time, we must specify a type attribute for the images. This is so Falagard knows where to place this image in the frame. These are of course listed in the Falagard XML Enumeration Reference.
The format both horizontally and vertically is Stretched. This ensures that the background image is stretched to fill as much as possible of the "hole" created by the border images (TL,T,TR etc.).
We'll use this frame for all the 3 states (Enabled, Disabled, ReadOnly), but we want the colours to be a little different in each state. In part I we created a new property to hold the colour of the Text. To show a different approach, we'll "hard-code" the frame colours into this look'n'feel.
CEGUI supports using a colour rectangle to modulate the colours of anything it renders. This makes it possible to "apply" nice colour gradients to our frame imagery.
The Normal state will use the colours, unmodified from the image file.
The Disabled state will use gray to darken the frame imagery
In the ReadOnly state we'll create a gradient that is white in the top-left corner, and slightly grayish for the three other corners. When applied to the imagery this will result in the top-left corner using exactly the same colours as in the image file, and the three other corners being slightly darkened.
To use this frame component we must - like stated earlier - fit it in a ImagerySection. We'll call this new imagery section "frame".
This is all the we need to write up the xml for this frame, and use it in our states, so now the XML looks like this:
<?xml version="1.0" ?> <Falagard> <b><ImagerySection name="frame"> <FrameComponent> <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 type="TopLeftCorner" imageset="MyImages" image="EditboxTL" /> <Image type="TopEdge" imageset="MyImages" image="EditboxT" /> <Image type="TopRightCorner" imageset="MyImages" image="EditboxTR" /> <Image type="RightEdge" imageset="MyImages" image="EditboxR" /> <Image type="BottomRightCorner" imageset="MyImages" image="EditboxBR" /> <Image type="BottomEdge" imageset="MyImages" image="EditboxB" /> <Image type="BottomLeftCorner" imageset="MyImages" image="EditboxBL" /> <Image type="LeftEdge" imageset="MyImages" image="EditboxL" /> <Image type="Background" imageset="MyImages" image="EditboxBg" /> <VertFormat type="Stretched" /> <HorzFormat type="Stretched" /> </FrameComponent> </ImagerySection></b> <ImagerySection name="Carat"> </ImagerySection> <StateImagery name="Enabled"> <b><Layer> <Section section="frame" /> </Layer></b> </StateImagery> <StateImagery name="Disabled"> <b><Layer> <Section section="frame"> <Colours topLeft="FF7F7F7F" topRight="FF7F7F7F" bottomLeft="FF7F7F7F" bottomRight="FF7F7F7F" /> </Section> </Layer></b> </StateImagery> <StateImagery name="ReadOnly"> <b><Layer> <Section section="frame"> <Colours topLeft="FFFFFFFF" topRight="FFDFDFDF" bottomLeft="FFDFDFDF" bottomRight="FFDFDFDF" /> </Section> </Layer></b> </StateImagery> <StateImagery name="ActiveSelection"> </StateImagery> <StateImagery name="InactiveSelection"> </StateImagery> </Falagard>
The frame component has been added, and the Enabled, Disabled and ReadOnly states have been set up to use them. The only new thing is really the Colours tag inside the Section tags for "frame".
It behaves just like the ColourProperty tag we used in part I, except you can specify a colour to use for each corner. It's attributes
- topLeft
- topRight
- bottomLeft
- bottomRight
Each specify a single colour of the same format we used for ColourProperty (AARRGGBB in hex).
Text
Remeber something about a NamedArea? Either way we're going to look at it now. The Editbox widget requires us to define a named area so it knows where to render, the text we type in, the caret (the marker that shows us where the characters we type will be inserted) and our selection.
In this look'n'feel, we want the text to be rendered inside our frame. That is we dont want the text to cover the frame imagery itself, only the background of it. Falagard provides a ImageDim tag that we can use to extract the width and height from the images we used for the frame. Falagard also provides a DimOperator tag that we can use to do a little math on our dimensions. These features will be the core of our NamedArea which will be named TextArea (Stated in the requirements... Remember?)
The only thing we must (and may) specify in a NamedArea is a Area.
If we take a look back at the Area tag, we know that it must specify four dimensions. LeftEdge, TopEdge, RightEdge and BottomEdge. You might remember that we can specify Width instead of RightEdge, and Height instead of BottomEdge. But we're not going to do that. Just so you know (and don't forget) ;-)
We start out with a "empty" XML and fill it out as we go. For now it looks like this:
<NamedArea name="TextArea"> <Area> <Dim type="LeftEdge"> </Dim> <Dim type="TopEdge"> </Dim> <Dim type="RightEdge"> </Dim> <Dim type="BottomEdge"> </Dim> </Area> </NamedArea>
We start out with the left edge. In the frame we used the image named EditboxL for the left edge. And as the area we want to define is to lie inside the frame, we'll use the width of this specific image as the left edge of our area. The XML for this ImageDim is like so:
<ImageDim imageset="MyImages" image="EditboxL" dimension="Width" />
The top edge is very similar except we'll use the height of the image named EditboxT instead.
<ImageDim imageset="MyImages" image="EditboxT" dimension="Height" />
The right edge is a bit more tricky. To start out we'll use a UnifiedDim that gives us the right edge of our widget.
<UnifiedDim scale="1" offset="0" type="RightEdge"> </Unified>
We use a seperate tag to close it, as by itself this UnifiedDim is not good enough. We still need the take the frame into accout. The image dimension we're after is the width of the image EditboxR. We must subtract this width from the right edge to ensure that the frame imagery is not overwritten by a long text string in the Editbox.
The DimOperator can do this for us with XML like this:
<UnifiedDim scale="1" offset="0" type="RightEdge"> <DimOperator op="Subtract"> <ImageDim imageset="MyImages" image="EditboxR" dimension="Width" /> </DimOperator> </Unified>
The DimOperator tag requires us to specify the attribute op with the name of the operation we want to perform as the value. The operations supported by Falagard are:
- Add
- Subtract
- Multiply
- Divide
We used Subtract to "move back" a little from our right edge to make sure we leave the frame imagery for the right edge alone.
The bottom edge of the area is very similar to the right. There are two differences. First we want the UnifiedDim to take the bottom edge of our window and not the right edge. Second we want to subtract the height of the image named EditboxB instead too.
This gives us this XML:
<UnifiedDim scale="1" offset="0" type="BottomEdge"> <DimOperator op="Subtract"> <ImageDim imageset="MyImages" image="EditboxB" dimension="Height" /> </DimOperator> </Unified>
If we put these four dimensions into our NamedArea it looks like this:
<NamedArea name="TextArea"> <Area> <Dim type="LeftEdge"> <b><ImageDim imageset="MyImages" image="EditboxL" dimension="Width" /></b> </Dim> <Dim type="TopEdge"> <b><ImageDim imageset="MyImages" image="EditboxT" dimension="Height" /></b> </Dim> <Dim type="RightEdge"> <b><UnifiedDim scale="1" offset="0" type="RightEdge"> <DimOperator op="Subtract"> <ImageDim imageset="MyImages" image="EditboxR" dimension="Width" /> </DimOperator> </Unified></b> </Dim> <Dim type="BottomEdge"> <b><UnifiedDim scale="1" offset="0" type="BottomEdge"> <DimOperator op="Subtract"> <ImageDim imageset="MyImages" image="EditboxB" dimension="Height" /> </DimOperator> </Unified></b> </Dim> </Area> </NamedArea>
This is the complete NamedArea for our Editbox look'n'feel. If we fit it inside our full look'n'feel it will look like the XML below. Notice that the NamedArea is specifed before the ImagerySection. This is required by Falagard.
<?xml version="1.0" ?> <Falagard> <b><NamedArea name="TextArea"> <Area> <Dim type="LeftEdge"> <ImageDim imageset="MyImages" image="EditboxL" dimension="Width" /> </Dim> <Dim type="TopEdge"> <ImageDim imageset="MyImages" image="EditboxT" dimension="Height" /> </Dim> <Dim type="RightEdge"> <UnifiedDim scale="1" offset="0" type="RightEdge"> <DimOperator op="Subtract"> <ImageDim imageset="MyImages" image="EditboxR" dimension="Width" /> </DimOperator> </Unified> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge"> <DimOperator op="Subtract"> <ImageDim imageset="MyImages" image="EditboxB" dimension="Height" /> </DimOperator> </Unified> </Dim> </Area> </NamedArea></b> <ImagerySection name="frame"> ... ... saving space ... ... </ImagerySection> <ImagerySection name="Carat"> </ImagerySection> <StateImagery name="Enabled"> <Layer> <Section section="frame" /> </Layer> </StateImagery> <StateImagery name="Disabled"> <Layer> <Section section="frame"> <Colours topLeft="FF7F7F7F" topRight="FF7F7F7F" bottomLeft="FF7F7F7F" bottomRight="FF7F7F7F" /> </Section> </Layer> </StateImagery> <StateImagery name="ReadOnly"> <Layer> <Section section="frame"> <Colours topLeft="FFFFFFFF" topRight="FFDFDFDF" bottomLeft="FFDFDFDF" bottomRight="FFDFDFDF" /> </Section> </Layer> </StateImagery> <StateImagery name="ActiveSelection"> </StateImagery> <StateImagery name="InactiveSelection"> </StateImagery> </Falagard>
We don't reference TextArea anywhere. Just specifying it is all that is required according to the Falagard System base widgets reference.
Caret
The Editbox has a caret. This is a marker that shows us where we are typing and allows us to tell if we are inserting in the middle of the text or at the end etc. The text is split around it by CEGUI so we don't draw over the text rendering the point we're editing unreadable. All this is currently handled internally and all you need to do is create a ImagerySection name Carat.
note by Pompei2: You're reading right, the ImagerySection is named Carat although the right word is a caret.
When CEGUI needs render the caret (and the text) it will fetch this ImagerySection and render it at the appropriate position in the text. We do not need to make a TextComponent in a Editbox to get the text to render. Because of the pecularities of the caret (and selection which we will handle later) it's all done automatically, by using the NamedArea TextArea we just specified along with the real pixel area of the window, to confine rendering to right rectangle on the screen.
Enough details. We just want to render a single image named EditboxCaret stretched vertically inside the TextArea.
Creating the ImagerySection is very much like creating the background for the Button in part I, the only real difference being the Area used. We'll look at the XML for this area, it's all made up by stuff we've already seen.
<Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="Width"> <ImageDim imageset="MyImages" image="EditboxCaret" dimension="Width" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area>
The first and second (left/top edges) Dims have the familiar AbsoluteDim with a value of zero. Nothing new here.
The third Dim on the other hand is new. It's now specified as "Width" and not "RightEdge" like we have done so far. Inside we have a ImageDim for the width of the caret image we're using. If we had used the regular UnifiedDim, the caret itself would have the width our textarea (because the caret resides in the text area) and would have been very ugly...
The fourth dimension is the familiar UnifiedDim we have used in all the earlier areas.
We know from part I how to make a ImagerySection with a ImageryComponent. This is all we need to render this single-image caret. The XML for our look'n'feel is nearing completion.
<?xml version="1.0" ?> <Falagard> <NamedArea name="TextArea"> ... ... saving space ... ... </NamedArea> <ImagerySection name="frame"> ... ... saving space ... ... </ImagerySection> <ImagerySection name="Carat"> <b><ImageryComponent> <Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="Width"> <ImageDim imageset="MyImages" image="EditboxCaret" dimension="Width" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area> <Image imageset="MyImages" image="EditboxCaret" /> <VertFormat type="Stretched" /> </ImageryComponent></b> </ImagerySection> <StateImagery name="Enabled"> <Layer> <Section section="frame" /> </Layer> </StateImagery> <StateImagery name="Disabled"> <Layer> <Section section="frame"> <Colours topLeft="FF7F7F7F" topRight="FF7F7F7F" bottomLeft="FF7F7F7F" bottomRight="FF7F7F7F" /> </Section> </Layer> </StateImagery> <StateImagery name="ReadOnly"> <Layer> <Section section="frame"> <Colours topLeft="FFFFFFFF" topRight="FFDFDFDF" bottomLeft="FFDFDFDF" bottomRight="FFDFDFDF" /> </Section> </Layer> </StateImagery> <StateImagery name="ActiveSelection"> </StateImagery> <StateImagery name="InactiveSelection"> </StateImagery> </Falagard>
We only specify the vertical formatting for the new ImageryComponent because we are specifying the area to exactly match the width of the image, and thus any formatting would have no effect. LeftAligned (the default) is just fine.
Now we just need to do something in the ActiveSelection and InactiveSelection states...
Selection
When the user has selected text in the Editbox, either with the mouse or the keyboard, CEGUI render the selection using the two additional states ActiveSelection and InactiveSelection.
The the window is active (has focus) it will use the state ActiveSelection, if if does not have focus (inactive) yet still has a selection InactiveSelection will be used.
For the selection in this look'n'feel, we'll use a single image stretched over the selection area. This image will contain plain white in the image file so we can apply any colour(s) we choose to the selection imagery. Let's call it White.
For ActiveSelection we'll use a classic blue colour, for inactive the same, except we'll make it 50% transparent. We can avoid a colour tag in the ActiveSelection state if we specify the colours to use directly in the ImageryComponent. We'll do that to show how it works.
We'll create a new ImagerySection for this "selection brush" named selection. XML follows:
<ImagerySection name="selection"> <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="White" /> <Colours topLeft="FF26458A" topRight="FF26458A" bottomLeft="FF26458A" bottomRight="FF26458A" /> <VertFormat type="Stretched" /> <HorzFormat type="Stretched" /> </ImageryComponent> </ImagerySection>
We use a default area that you should recognise by now. CEGUI will automatically use the right size for the selection. The image we use is the one called White from our favorite imageset MyImages. Like always ;-) A colour tag has been added as well to make our selection some kind of blue... The vertical and horizontal formatting is Stretched which is generally what you would want to do in a situation like this.
We'll "call" this ImagerySection from the ActiveSelection and InactiveSelection states. In the InactiveSelection state we'll further modulate the colours used by passing a 50% transparent white. This will have the effect of using the same colour as in the active state, but make that colour 50% transparent. We could have just specified the full colour to use in each state and remove the Colours tag from the ImageryComponent, but I wanted to show that multiple modulations are possible with Falagard.
The states are the last thing missing from the look'n'feel, so let's take a look at the final XML (yay :-P ).
<?xml version="1.0" ?> <Falagard> <NamedArea name="TextArea"> <Area> <Dim type="LeftEdge"> <ImageDim imageset="MyImages" image="EditboxL" dimension="Width" /> </Dim> <Dim type="TopEdge"> <ImageDim imageset="MyImages" image="EditboxT" dimension="Height" /> </Dim> <Dim type="RightEdge"> <UnifiedDim scale="1" offset="0" type="RightEdge"> <DimOperator op="Subtract"> <ImageDim imageset="MyImages" image="EditboxR" dimension="Width" /> </DimOperator> </Unified> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge"> <DimOperator op="Subtract"> <ImageDim imageset="MyImages" image="EditboxB" dimension="Height" /> </DimOperator> </Unified> </Dim> </Area> </NamedArea> <ImagerySection name="frame"> <FrameComponent> <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 type="TopLeftCorner" imageset="MyImages" image="EditboxTL" /> <Image type="TopEdge" imageset="MyImages" image="EditboxT" /> <Image type="TopRightCorner" imageset="MyImages" image="EditboxTR" /> <Image type="RightEdge" imageset="MyImages" image="EditboxR" /> <Image type="BottomRightCorner" imageset="MyImages" image="EditboxBR" /> <Image type="BottomEdge" imageset="MyImages" image="EditboxB" /> <Image type="BottomLeftCorner" imageset="MyImages" image="EditboxBL" /> <Image type="LeftEdge" imageset="MyImages" image="EditboxL" /> <Image type="Background" imageset="MyImages" image="EditboxBg" /> <VertFormat type="Stretched" /> <HorzFormat type="Stretched" /> </FrameComponent> </ImagerySection> <ImagerySection name="Carat"> <ImageryComponent> <Area> <Dim type="LeftEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="TopEdge"> <AbsoluteDim value="0" /> </Dim> <Dim type="Width"> <ImageDim imageset="MyImages" image="EditboxCaret" dimension="Width" /> </Dim> <Dim type="BottomEdge"> <UnifiedDim scale="1" offset="0" type="BottomEdge" /> </Dim> </Area> <Image imageset="MyImages" image="EditboxCaret" /> <VertFormat type="Stretched" /> </ImageryComponent> </ImagerySection> <ImagerySection name="selection"> <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="White" /> <Colours topLeft="FF26458A" topRight="FF26458A" bottomLeft="FF26458A" bottomRight="FF26458A" /> <VertFormat type="Stretched" /> <HorzFormat type="Stretched" /> </ImageryComponent> </ImagerySection> <StateImagery name="Enabled"> <Layer> <Section section="frame" /> </Layer> </StateImagery> <StateImagery name="Disabled"> <Layer> <Section section="frame"> <Colours topLeft="FF7F7F7F" topRight="FF7F7F7F" bottomLeft="FF7F7F7F" bottomRight="FF7F7F7F" /> </Section> </Layer> </StateImagery> <StateImagery name="ReadOnly"> <Layer> <Section section="frame"> <Colours topLeft="FFFFFFFF" topRight="FFDFDFDF" bottomLeft="FFDFDFDF" bottomRight="FFDFDFDF" /> </Section> </Layer> </StateImagery> <StateImagery name="ActiveSelection"> <Layer> <Section section="selection" /> </Layer> </StateImagery> <StateImagery name="InactiveSelection"> <Layer> <Section section="selection"> <Colours topLeft="7FFFFFFF" topRight="7FFFFFFF" bottomLeft="7FFFFFFF" bottomRight="7FFFFFFF" /> </Section> </Layer> </StateImagery> </Falagard>
That's it folks. The look'n'feel is done. Hopefully you're starting to play around with the XML and making the look'n'feel suit you (or your artist ;-) ). Again, feel free to add comments in the discussion page.
Stay tuned for part III...
--User:lindquist 00:41, 14 Dec 2005 (CET)