Using the window hierarchy

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.

Getting ready

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.

How to do it…

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.

Child window rendering

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.

Event propagation

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

How it works…

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.

See also

  • The Drawing a simple window recipe
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.170.63