Tabletop Simulator

Tabletop Simulator

Not enough ratings
The art of scripting: XML Interfaces Part II: Building a basic window
By Floaty
This guides explains how one can build up a basic window for custom UI and give it functionalities such as minimizing, closing and the ability to be dragged.
   
Award
Favorite
Favorited
Unfavorite
Start
In this tutorial we will build a basic window with common functions such as the ability to minimize and to be closed. It will have common window bar and will show its title. This window can be used as a setup to create custom UIs.

What UI elements will be used in this tutorial?
  • Panel
  • Text
  • Button


The final code:
XML
<!-- Defining the standard appearance of the window --> <Defaults> <Panel class="Window" width="385" height="300" rectAlignment="MiddleRight" returnToOriginalPositionWhenReleased="false" allowDragging="true" color="#69696940" outline="#404040"/> <Panel class="TopBar" height="30" width="385" rectAlignment="UpperCenter" color="#19197040" outline="#00008B"/> <Button class="topButtons" width="20" height="20" rectAlignment="UpperRight" color="#eeeeee"/> <Text class="WindowTitle" text="Window" fontSize="18" height="20" fontStyle="Bold" rectAlignment="UpperCenter" color="#FFFFFF" /> </Defaults> <!-- The layout of the window --> <Panel class="Window" id="Window"> <Panel class="TopBar" id="TopBar"/> <Text id="WindowTitle" class="WindowTitle" offsetXY="0 -5"/> <Button id="minimizeButton" class="topButtons" textColor="#000000" text="_" offsetXY="-25 -5" onClick="minimize"/> <Button id="closeButton" class="topButtons" color="#990000" textColor="#FFFFFF" text="X" offsetXY="-5 -5" onClick="close"/> </Panel>

Lua script
-- Function to minimize the window function minimize(player, value, id) -- Is the size of the window bigger than its top bar? if(tonumber(UI.getAttribute("Window", "Height")) > tonumber(UI.getAttribute("TopBar", "Height"))) then UI.setAttribute("Window","OffsetXY", "0 135") -- Offset is set to (Height - Height_of_Top_Bar) /2 UI.setAttribute("Window", "Height", tonumber(UI.getAttribute("TopBar", "Height"))) else UI.setAttribute("Window","OffsetXY", "0 0") UI.setAttribute("Window", "Height", 300) end end -- Function to close the window function close(player, value, id) UI.hide("Window") end
What UI elements will be used in this tutorial?
  • Panel
  • Text
  • Button
Setting up the window
Before we can do anything, we need to set up our window. As our basic layouting element, we use the Panel, because it gives us the highest degree of freedom to position and size its child elements (one could argue that for our task the VerticalLayout may be useful, but as if have stated in the first part of this series, this layout is, sadly, not useful for cases like this because it resizes every child element to fit its space). To set up the window, we firstly define a panel with defined dimensions and a color (otherwise it would be simply invisible).

<Panel color="white" height="300" width="385"/>

This gives us the following window:


Not very exciting, eh?
We will now add typical window top bar with two buttons and a text to display the title of the window. For this task we will define another panel, together with a text element and two buttons:
<Panel class="Window" id="Window" color="white" width="385" height="300"> <Panel class="TopBar" id="TopBar" height="20" color="black"> <Text color="white" >Window Title</Text> <Button width="20" height="20" >_</Button> <Button width="20" height="20" >X</Button> </Panel> </Panel>
This will result in the following window:

That is also only a slight improvement to before. Also, our xml starts to get really messy. That's a good chance to clean up our xml and define a standard appearance via defaults:
<Defaults> <Panel class="Window" color="White" width="385" height="300"/> <Panel class="TopBar" color="black" height="20" /> <Text class="WindowTitle" text="Window Title"/> <Button class="topButton" width="20" height="20"/> </Defaults>
After assigning the right classes, our code will now look a lot cleaner:
<Panel class="Window" id="Window"> <Panel class="TopBar" id="TopBar"> <Text class="WindowTitle"/> <Button class="topButton">_</Button> <Button class="topButton">X</Button> </Panel> </Panel>

We now have to get our top bar to the actual top to make this mess look like a normal window. Also the buttons have to be moved to the right and separated. For this, we change the defaults we have set up to accomodate for our new needs:
<Defaults> <Panel class="Window" color="White" width="385" height="300"/> <Panel class="TopBar" color="black" height="20" rectAlignment="UpperCenter"/> <Text class="WindowTitle" text="Window Title" rectAlignment="UpperCenter"/> <Button class="topButton" width="20" height="20" rectAlignment="UpperRight/> </Defaults>



To separate the buttons, the left (minimize) button needs to get an offset. Because we want to move it to the left, it will get a negative offset for X-axis:
<Button class="topButton" offsetXY="-20 0">_</Button>

Now it looks a lot nicer:



But without some color and fancy effects is looks rather boring, don't you think?
So i think we can now start to set some colors. For main window, we will use a darker grey which is, in my opinion, much less "agressive" than plain white. Concerning the buttons we will only color the closing button red, because a closing button is always red. Also we need to change the color of the text to make it more visible (and also tweak around with its style and font size). And the top bar needs another color, plain black is simply boring. Every color is possible, i personally prefer could old midnight blue:
<Defaults> <Panel class="Window" width="385" height="300" color="#696969"/> <Panel class="TopBar" height="30" rectAlignment="UpperCenter" color="#191970"/> <Text class="WindowTitle" text="Window" fontSize="18" height="20" fontStyle="Bold" rectAlignment="UpperCenter" color="#FFFFFF" /> <Button class="topButton" width="20" height="20" rectAlignment="UpperRight" color="#eeeeee"/> </Defaults>

We will also tweak the positioning of the buttons and the title a little more to give the top bar a nicer look:
<Panel class="Window" id="Window"> <Panel class="TopBar" id="TopBar"> <Text class="WindowTitle" offsetXY="0 -5"/> <Button class="topButton" offsetXY="-25 -5">_</Button> <Button class="topButton" offsetXY="-5 -5" textColor="White" color="red">X</Button> </Panel> </Panel>



Before we start the scripting to make our window functional, we need to adjust a few more things:
Every user would be annoyed if the window would pop up in the middle of screen and he or she not even able to move it, right?
We can simply change that via fiddling around with the default for our window:
<Panel class="Window" width="385" height="300" color="#696969" allowDragging="True" returnToOriginalPositionWhenReleased="false" rectAlignment="MiddleRight"/>
What have we done? Via setting the allowDragging attribute to true, we make the panel and thus its child elements dragable. To let the user actually decide where the window should go, we also need to set the returnToOriginalPositionWhenReleased attribute to false. Our last change sets the starting position of the window to the middle right of the screen:


We should also at a little boundary to our window and top bar to make it look better. This can be done via the outline attribute:
<Panel class="Window" width="385" height="300" color="#696969" outline="#404040" allowDragging="True" returnToOriginalPositionWhenReleased="false" rectAlignment="MiddleRight" outline="#404040"/> <Panel class="TopBar" height="30" width="385" rectAlignment="UpperCenter" color="#191970" outline="#00008B"/>

The changes are small, but visible:



Now we could be done but allow me to propose one last little change to finish the looks of our window: Transparency. This can be easily added by changing the color attribute of both the window and the top bar. We will set the opacity to 25% to give it a nice look:

<Panel class="Window" width="385" height="300" color="#69696940" allowDragging="True" returnToOriginalPositionWhenReleased="false" rectAlignment="MiddleRight" outline="#404040"/> <Panel class="TopBar" height="30" width="385" rectAlignment="UpperCenter" color="#19197040" outline="#00008B"/>

Making the window functional
Now the only thing left to do is to make the window actually functional. At first we have to tell the buttons which method they have to call upon a click. This can be done via setting the onClick attribute:

<Button id="minimizeButton" class="topButtons" textColor="#000000" text="_" offsetXY="-25 -5" onClick="minimize"/> <Button id="closeButton" class="topButtons" color="#990000" textColor="#FFFFFF" text="X" offsetXY="-5 -5" onClick="close"/>

The buttons will now call the Lua functions minimize and close upon a click. Now let us design the appropriate functions to catch those calls:

-- Function to minimize the window function minimize(player, value, id) -- TODO end -- Function to close the window function close(player, value, id) -- TODO end

To close the window is the easy part: We have just to tell the UI to hide it. This can be done via calling:
UI.hide("Window")

The UI will now look for an element with the ID Window and will hide it, making it completely invisible.

The more complicated part is minimizing functionality. But that is nothing we cannot accomplish:

function minimize(player, value, id) -- Is the size of the window bigger than its top bar? if(tonumber(UI.getAttribute("Window", "Height")) > tonumber(UI.getAttribute("TopBar", "Height"))) then UI.setAttribute("Window","OffsetXY", "0 135") -- Offset is set to (Height - Height_of_Top_Bar) /2 UI.setAttribute("Window", "Height", tonumber(UI.getAttribute("TopBar", "Height"))) else UI.setAttribute("Window","OffsetXY", "0 0") UI.setAttribute("Window", "Height", 300) end end



What are we doing here? At first we check if the window is bigger that its top bar by comparing both height attributes of the elements. If it is bigger, it height will be adjusted to the height of top bar. Because that would cause movement of the window, we also need adjust its offset to prevent that from happening. If the window is minimized, we will set its offset to 0 and restore its original height.
Closing
Together we designed a fully functional window as a base for more sophisticated UIs. However, a few problems were left unaddressed: The minimizing function is nice proof of concept, but it will most likely not work if the panel contains more content than just the top bar. This problem could be solved by grouping the content in another element, which would allow to hide this element prior to the minimization process.

If you want to try out the created window, it can be found here.
Q & A
How does the transparency change work?
To understand how we have set the transparency, we first have to understand how the colors are encoded:
A normal color can take values between the following (hexadecimal) values: 00000000 and FFFFFFFF. But what does that mean? This actually encodes a color in the so called RGBA color space, where R stands for red, G stands for green, B stands for blue and A stands for alpha. 2 characters will encode each channel of the color, which can take values between 00 (the channel does not contribute to the color at all) and FF (=255, meaning the channel contributes fully). The only thing we have done is to change the (optional) alpha value of the color to 40 (=64, meaning the channel only contributes 25% percent (64/255 = 0.25) to the color. That means that the color is 25% opac or 75% transparent.

I don't understand how you determined the offset for the minimizing!
After the minimization, the game will position the window again to its assigned position, in our case to the MiddleCenter. How does that effect the position of our window? Because its now much smaller, the window will "jump" a little across the screen because its center is now shifted (the element position seems to be determined to according to the center of the element). But how can we avoid that?
To avoid the jumping, we have to adjust the position after the minimization.
We have calculated the value with following formula:
(Height - Height_of_Top_Bar) /2
But why does that work? To know that, we have to determine how much away our top bar is from the center of our window. This amount is easy to calculate: The top of the window would be simply Height/2 distanced from the center of the window. But we have to also regard the size of out top bar. Because the top cuts away some distance to the center, it needs to be subtracted before we calculate our shift.

Am i allowed to use this (or parts of it) in my mod?
Of course. It would be nice if you would mention me, but i will no chase you if you don't.
9 Comments
Ohad\moxtar 12 Apr, 2020 @ 1:53pm 
fix some bugs:
change twice: <Panel . . . /> to <Panel . . . >
add: id="Window"
add: id="TopBar"
change: class="topButton" not class="topButtons"

strelok-halfer 7 Oct, 2019 @ 1:29pm 
Seems bug with HEIGHT attribute, changed 20 to 30 and its fixed.
ChizBallz 21 Jan, 2019 @ 3:31pm 
I tried to unsubscribe and resubscribed to this mod, here's what it looks like

https://i.postimg.cc/85mXN0G7/image.png
Floaty  [author] 21 Jan, 2019 @ 3:16pm 
I tested it, it works at least on my PC.
ChizBallz 21 Jan, 2019 @ 2:09pm 
Could you accept my friend request. I tried adding you
ChizBallz 21 Jan, 2019 @ 1:03pm 
Nope, not even the example one shows up either. I'm not sure why. If you do figure it out, please let me know. Thanks.
Floaty  [author] 21 Jan, 2019 @ 9:06am 
That's odd. Does the example work if you install it as "mod" from the workshop?
ChizBallz 20 Jan, 2019 @ 9:34pm 
The Window text isn't showing up in the topbar for me. Any ideas?
Mellester1 4 Oct, 2018 @ 11:07am 
Nice guide