A basic game UI can be constructed with a bunch of windows that are on the same level. There might be occasions where you'll need more complex UI with layered windows. You can observe this kind of UI in many modern window managers or web browsers. Windows can contain other windows or user controls. If you move the main window, it will also move inner elements with it. This behavior is done with the window hierarchy.
Implementing the window hierarchy system requires a well-defined data structure design, as well as correct use of matrix transformations.
This recipe will use a simple graph structure that consists of nodes and a list of child nodes. You can implement this structure in the Lua language with tables:
local main_window = { properties = {}, children = { { properties = {}, children = {}, }, -- ... }, }
In this case, each node contains window properties and a list of children windows. With this design, you can traverse all windows in one direction—from parent windows to child windows.
In this recipe, you'll be using a combination of tables and closures to define the window hierarchy. Every window functionality will be defined inside the window object closure. This will make things easier to maintain in further steps. The following code shows an example of the window hierarchy with two levels defined by the table structure:
--[[ gui table contains all functions to create window object closures --]] local gui = {} local main_window = gui.window { properties = { width = 128, height = 128, x = 0, y = 0, z = 0, }, children = { gui.window { properties = { width = 128, height = 128, x = 32, y = 0, z = 0, }, children = {}, }, }, }
There are several points of view in window hierarchy implementations. The first one deals with drawing child windows correctly. Also, if the parental window is invisible, child windows should be invisible too.
Another point of view addresses event propagation to child windows.
Each window uses its own model-view matrix to draw the window on screen at the desired position. From this point, window rendering is fairly easy to implement. The only problem is how to obtain the correct form of model-view matrix so that the child window is always relative to its parent window. This can be solved by a hierarchically propagated update of the model-view matrix of children windows when you update the parent windows' parameters. This way you can assure that each child window honors the model-view matrix of its parent window and it prevents the application of unwanted side effects such as child window stretching when you resize its parent window.
This might seem to be an expensive operation but take into account that it uses the tree structure to eliminate unnecessary updates and additionally, such window updates do not occur very often.
The following sample code uses the object closure approach so that each window object can be constructed with a single function call and a single table that contains the initial window parameters. You'll see the benefits of this later in the code:
-- def – window definition gui.window = function(def) -- window object instance local obj = {} local prop = def.properties obj.properties = props local children = def.children -- computed model-view matrix local modelViewMatrix -- window visibility property if type(def.visible)=="boolean" then obj.visible = prop.visible else obj.visible = true end -- event propagation for window if type(def.enabled)=="boolean" then obj.enabled = prop.enabled else obj.enabled = true end --[[ updates model-view matrices - function parameters are expected to be matrices, these will be also used in addition to local model-view matrix --]] obj.update = function(...) local outermatrices = {...} -- reset model-view matrix to identity modelViewMatrix = matrix.dup() local scaleMatrix = S(prop.width or 1, prop.height or 1, 1) -- invScaleMatrix prevents of unwanted side-effect propagation local invScaleMatrix = scaleMatrix.inv() table.insert(outerMatrices, T(prop.x or 0, prop.y or 0, prop.z or 0) * R(prop.rotateX or 0, prop.rotateY or 0, prop.rotateZ or 0, prop.rotateAngle or 0) * scaleMatrix * T(prop.originX or 0, prop.originY or 0, prop.originZ or 0) ) for _, m in ipairs(outerMatrices) do modelViewMatrix = modelViewMatrix * m end for i, child in ipairs(children) do local prop = child.properties child.update(modelViewMatrix, T((prop.relativeX or 0), (prop.relativeY or 0), (prop.relativeZ or 0)), invScaleMatrix) end obj.modelViewMatrix = modelViewMatrix end obj.draw = function() if obj.enabled then -- apply shader program for GUI if it's not already used if obj.visible then -- draw window with current model-view matrix -- ...window rendering code... for _, child in ipairs(children) do child.draw() end end end end -- prepare model-view matrix before first use obj.update() return obj end
Notice that window rendering relies on two functions, update
and draw
. The update
function generates the model-view matrix for the window and its children. The draw
function simply draws the current window and the same process is recursively repeated on the child windows.
Windows and user controls need some form of interaction. This is usually achieved with the event system. This recipe will use the so-called signal slots. Each signal slot represents a specific type of the event and it consists of a list of functions that will be called consecutively. You can implement this by extending the window creation routine with signal storage and three functions, namely, propagateSignal
, addSignal
, and callSignal
. The following sample code shows the basis of this implementation:
gui.window = function(def) -- ...previous code for window object initialization local signals = {} --[[ list of events are invoked only if mouse cursor is over window --]] local onWindowEvents = { SDL.SDL_MOUSEMOTION, SDL.SDL_MOUSEBUTTONDOWN, SDL.SDL_MOUSEBUTTONUP, } -- does this window have a focus? obj.focused = false obj.propagateSignal = function(name, ...) if obj.enabled then local propagate, callSignal = true, true for _, eventName in ipairs(onWindowEvents) do if eventName == name then local mouse_x, mouse_y = unpack {...} if obj.isMouseOverWindow(mouse_x, mouse_y) then propagate = false else callSignal = false end break end end for _, child in ipairs(children) do if not child.propagateSignal(name, ...) then return false end end if callSignal then obj.callSignal(name, ...) end return propagate else return false end end obj.callSignal = function(name, ...) local list = signals[name] if type(list)=="table" then for i, action in ipairs(list) do if type(action)=="function" then if not action(obj, ...) then return false end end end end return true end obj.addSignal = function(name, fn) if not signals[name] then signals[name] = {} end local list = signals[name] if type(list)=="table" and type(fn)=="function" then table.insert(list, fn) end end obj.projectMouseCursorToWindow = function(mouse_x, mouse_y) local relativeMouseCoords = modelviewMatrix.inv() * {mouse_x, mouse_y, 0, 1} local T,S = matrix.translate, matrix.scale local originMatrix = T(prop.originX or 0, prop.originY or 0, 0) local scaleMatrix = S(prop.width or 0, prop.height or 0, 1) local mouseCoordsOnWindow = scaleMatrix * originMatrix * relativeMouseCoords return mouseCoordsOnWindow[1], mouseCoordsOnWindow[2] end obj.isMouseOverWindow = function(mouse_x, mouse_y) local relativeMouseCoords = modelviewMatrix.inv() * {mouse_x, mouse_y, 0, 1} local wx,wy = relativeMouseCoords[1], relativeMouseCoords[2] return (wX<=0.5 and wX>=-0.5 and wY<=0.5 and wY>=-0.5) end -- handle window focus state obj.addSignal(SDL.SDL_MOUSEBUTTONUP, function(self, x, y, button) if button==1 then if gui.focusedWindow then gui.focusedWindow.callSignal('lostFocus') gui.focusedWindow.focused = false end gui.focusedWindow = obj gui.focusedWindow.callSignal('focus') obj.focused = true end end) -- ...window object finalizer code return obj end
Note that the callSignal
function expects the signal functions to return a Boolean value. This helps to determine whether further signal functions should be called. This behavior allows you to literally consume the signal, if further processing of the event is not necessary.
However, for this to work, you'll need to modify the handleEvent
function to route the event into the main window:
local function handleEvent(name, ...) main_window.propagateSignal(name, ...) end
The window hierarchy is based on the tree structure. Each node is represented by the window or control elements. Each node can contain a list of children windows.
Note that window definitions use table structures. Each one consists of window properties and a list of children elements.
The drawing process paints the windows from the top to the bottom level. Each window has two Boolean flags that define the painting behavior. If the window has the enabled
flag set to false, it's invisible along with the children windows. On the other hand, if the window has the visible flag set to false, it's invisible
but the drawing process continues on its children windows. This way you can create window containers that aren't visible to the user but they can override the behavior of its children windows. This is useful for making window element groups or scrollable window content.
The good thing is that you can draw each window with the same set of vertices. The only thing that changes for each window is its model-view matrix. This allows you to avoid unnecessary CPU/GPU data transfer, which will slow down your game. However, this approach is valid only for windows that share the same window shape. This recipe uses a simple rectangular shape.
Events are defined by simple structures represented by signals. Each signal uses its own ordered list of functions. Signal functions return a Boolean value that determines whether the signal should be propagated further.
The propagateSignal
function is a bit more complex. It uses a list of events that are invoked only if the mouse cursor is over the current window. You must have noticed that it uses depth-first node traversal. This is especially useful when you click on a child window and you don't want the event to be propagated to parent windows. This will also ensure correct handling of the drag and drop feature for Windows.
18.223.206.69