An Introduction to Layouts in Quorum
What is a Layout?
In this tutorial, we will learn how to use Layouts in the Quorum Game Engine. Layouts are interface tools which tell a container where and how items in the game are shown on the screen.
There are two main layouts that you can use:
- ManualLayout – Layout where the positions and size of an item is defined by the item itself. This layout has no extra features and allows items to render where and how they want.
- FlowLayout – Layout where items have control of their sizes but not their positions. This layout has all the objects contained inside it start from the top left of the container and flow to the right. If the items overflow the width of the container, then they will by default flow onto another line.
What are LayoutProperties?
LayoutProperties are interface tools which allow you to preset how an Item2D, such as a Button or Icon, appears in the game. Controls like Buttons, Menus, and Icons have their own set of Default Properties that tells the game how they should be laid out. To alter how a control is laid out, you can either get the Control's current LayoutProperties and alter them or add your own LayoutProperties object for the Control to use instead. In this tutorial, we will show how to use both methods.
LayoutProperties also have several Layout Modes that tell the Control how it should calculate its dimensions either horizontally or vertically. There are five modes as follows:
- STANDARD - This layout mode calculates the dimensions and positions of the Control using percentages of the Control's dimensions combined with static pixel values. This mode is for if you want to set exactly how the Control should be sized.
- FILL - This layout mode will attempt to fill the remaining width or height of the container with the Control. Note this is only effective if the container's size is known independent of the element using this mode.
- FIT_CONTENTS - This layout mode will calculate this Control's dimensions as the combined total dimensions of its children elements.
- FIT_FONT - This layout mode is used to ensure a Control's height can fit the Font used in this LayoutProperties, plus padding from the children elements.
- MAINTAIN_ASPECT_RATIO - This layout mode is used to calculate one of the dimensions of the Control relative to the other dimension. This mode can only be used to calculate one of the two dimensions (width or height) at a time.
Laying out a Simple UI
In this tutorial, we will make a simple UI with two Buttons and an Icon. We will adjust the LayoutProperties of the three UI elements so that when the screen is resized, the elements will move or resize to adjust for the screen size change. We also make the Buttons change the properties of the Icon so the Icon changes position when the Buttons are clicked. To start, make a New Game Application.
Setting up the Window
For this tutorial, we will use the DesktopConfiguration, ManualLayout, LayoutProperties, Button, Icon and Color Libraries.
Before we create and adjust the Buttons, we will go to our main class, specifically the main action. Just before the StartGame() call, we'll use a DesktopConfiguration object to make our window resizable. This is so we can resize the window while the game is running, allowing us to see our Buttons and Icon change size and position in realtime based on the Screen size.
For a LayoutProperties ojbect to have an effect, the container for the UI elements needs to have a Layout set. In our case, the container will be the game itself, so we simply need to make a ManualLayout object and set the game's layout to be that object inside of the CreateGame action. Before we start adding new UI elements, our main class should look like this:
use Libraries.Game.Game use Libraries.Game.DesktopConfiguration use Libraries.Interface.Layouts.ManualLayout use Libraries.Interface.Layouts.LayoutProperties use Libraries.Interface.Controls.Button use Libraries.Interface.Controls.Icon use Libraries.Game.Graphics.Color class Main is Game action Main DesktopConfiguration config = GetDesktopConfiguration() config:resizable = true StartGame() end action CreateGame ManualLayout man SetLayout(man) end action Update(number seconds) end end
Making the UI Elements
Buttons are one of the most common elements that will appear in a user interface. In Quorum, Buttons have their own Layout and LayoutProperties that they use to position and display their contents. To change the properties of the Button, there are two primary methods, which also apply to all other controls. We can either call the Buttons actions like SetPercentageX() to change its default properties, or we make a LayoutProperties object and set it to the Button's DefaultProperties and change it from our object. The outcomes of both methods are the same, the only difference being that the Button does not have an action to change every aspect of the property, so there some properties you can only change through a LayoutProperties object.
Changing Properties with Public Actions
We will now make our first Button, and we will change its LayoutProperties using public actions. Note that from here on we will be adding code to the CreateGame action in our main class. First, we need to declare a Button and set its name to "Top Button." By default, a Button calculates its width using the FIT_CONTENTS mode and its height with the FIT_FONT mode. In this case we want to define the Size ourselves, so we will change both of these modes to STANDARD. These constants are not defined inside of the Button class, so we need to make a LayoutProperties object and access the constants from there. For this Button, we will change its percentage x and y, as well as its size in pixels. This is done with the following lines of code:
Button top LayoutProperties prop top:SetName("Top Button") top:SetHorizontalLayoutMode(prop:STANDARD) top:SetVerticalLayoutMode(prop:STANDARD) top:SetPercentageX(0.35) top:SetPercentageY(0.6) top:SetSize(250, 175)
Note the difference between Percentage and Pixel when using Layouts. For example, if a Control has a percentage width of 0.5 then the width of the Control will always be 50% of the width of its container. On the contrary, if you used SetPixelWidth or SetWidth and set the width to 300, then the Control will always be 300 pixels wide, regardless of the width of its container.
Using a LayoutProperites Object
Now we will make the second Button, and for this one we will use a LayoutProperties object to change the default properties of the Button. To get the default properties of the Button, we need to set our new object to the Button's properties using the GetDefaultLayoutProperties action. Then we use actions from the LayoutProperties object to change the Button. This is done with the following lines of code:
Button bottom bottom:SetName("Bottom Button") prop = bottom:GetDefaultLayoutProperties() prop:SetHorizontalLayoutMode(prop:STANDARD) prop:SetVerticalLayoutMode(prop:STANDARD) prop:SetPercentageX(0.5) prop:SetPercentageOriginX(0.5) prop:SetPercentageY(0.1) prop:SetPercentageWidth(0.3) prop:SetPercentageHeight(0.3)
Note in the above code that we used the LayoutProperties object from when we made the Top Button and that we used a new action. The SetPercentageOriginX changes the origin from where a Control calculates its position. This is a property that Buttons do not have a public action to change, so you would need to use this method to change that property.
That last Control we will add is an Icon and we will change its LayoutProperties differently from how we did the Buttons. Controls have multiple LayoutProperties and they will use them either based on the size of their container or which one was used most recently. For the Icon, we will make a LayoutProperties object, change its properties, and then add it to the Icon using the AddLayoutProperties action. We will also give the Icon a filled rectangle for its graphic. This is done in the following lines of code:
Icon selectBox Color color LayoutProperties boxProp selectBox:LoadFilledRectangle(10, 10, color:Blue()) boxProp:SetPercentageX(0.25) boxProp:SetPercentageY(0.55) boxProp:SetPercentageWidth(0.5) boxProp:SetPercentageHeight(0.4) selectBox:AddLayoutProperties(boxProp)
Now we'll add all three of the UI elements to the game so they appear when we run it. Note that we will add the Icon first, because we want it to be drawn under the Buttons.
Add(selectBox) Add(top) Add(bottom)
When you now run the game, there will be two Buttons and an Icon under one of them. But, if you were to resize the window, then the Bottom Button and the Icon will get larger or smaller depending on the size of the window. All three items will stay in the same relative position because we set their position to be based on a percentage. The Top Button will also stay the same size no matter the size of the window because we gave its dimensions in pixels.
Changing Properties with a Behavior
Now we have the Controls sized and positioned, but a good UI will respond when you interact with it, so we will make a Behavior for our two Buttons that will move the Icon depending on the position of the Button that pressed it.
We will have the Buttons take an Icon, which we will pass from our main class, get its current layout properties so it can change its y position, and then simply say and output that the Button was clicked. Previously we used the GetDefaultLayoutProperties, but since the object might not be using its default properties, we will use the GetCurrentLayoutProperties action to return the properties that the previous LayoutProperties was using to lay itself out.
The Behavior will be its own Quorum class known as ButtonBehavior and is as follows:
use Libraries.Interface.Behaviors.Behavior use Libraries.Interface.Events.BehaviorEvent use Libraries.Sound.Speech use Libraries.Interface.Controls.Icon use Libraries.Interface.Controls.Button use Libraries.Interface.Layouts.LayoutProperties class ButtonBehavior is Behavior Icon shape LayoutProperties prop action setShape(Icon icon) shape = icon end action Run(BehaviorEvent event) Button item = cast(Button, event:GetItem()) number pos = item:GetPercentageY() prop = shape:GetCurrentLayoutProperties() prop:SetPercentageY(pos - 0.05) Speech speech speech:Say(item:GetName() + " clicked", false) output item:GetName() + " clicked" end end
Now, with the Behavior complete, we can go back to our main class and set the shape for the Behavior and set the Behavior for the Buttons. This is done with the following lines of code:
ButtonBehavior behavior behavior:setShape(selectBox) top:SetBehavior(behavior) bottom:SetBehavior(behavior)
Now, when you run the program, the position and size of the items will still change with when the game is resized, but now clicking a Button will update the Layout of the Icon and change its y position.
In the above example, we used a ManualLayout to lay out the items in the UI, but the other main Layout you can use is the FlowLayout. As described previously, the Flow Layout allows for items to determine their own size, but the container and the layout determines their positions. The Flow Layout will force the children of the container to position themselves as far to the left as they can without overlapping other children, and if the width cannot contain them, then the items will try to form a new line under the full line and will start flowing to the right from there.
To demonstrate how a FlowLayout works, we will go back to our previous example and change the ManualLayout to a FlowLayout, which also means we need to include the FlowLayout library using the following line of code:
We will first set a FlowLayout to the game window by adding these next two lines of code to the top of the CreateGame action:
FlowLayout flow SetLayout(flow)
Now, we will change how the Top Button calculates its width to demonstrate how the FlowLayout repositions items. We will simply delete the SetSize call and give the Top Button the same percentage size as the Bottom Button. This is done with the following lines of code:
If you were to add up the percentage widths of the three items, you would notice that they add up to 1.1, which is greater than the width of the screen (1.0 means the entire width of the container). In this situation, the FlowLayout will move the item that will not fit in the line down to start its own line. In our example, that means that Bottom Button will be forced to its own line under the Icon.
Making a Container
In our example's current state, you'll notice that our action calls to change the position no longer work because of the FlowLayout. But, what if we want a ManualLayout for other UI elements? We can easily solve this by creating a container object to hold those items, and internally the container will have a FlowLayout but will exist in a ManualLayout. We can accomplish this with the following lines of code:
ManualLayout manual SetLayout(manual) Control container FlowLayout containFlow container:SetLayout(containFlow) container:SetPercentageWidth(1.0) container:SetPercentageHeight(0.4) container:SetPosition(50, 300) container:Add(selectBox) container:Add(top) container:Add(bottom) Add(container)
The above code will replace the add actions we had at the bottom of the CreateGame action.
Now, if you run the program, you will see that the Buttons and Icon are smaller since they are now part of a smaller container but they are in a FlowLayout while everything else added to the game is in a ManualLayout. This can be especially useful when adding things to your game like Menus, which are covered in a later tutorial, because they require a FlowLayout.
In the next tutorial, we will discuss Checkboxes, which describes how to use Checkboxes.