lovatt.dev

March 11th, 2019
11/03/19

Into The Dragon's Layer

In which we introduce a new class, remove heaps of logic from the GUI, and make the library's syntax much more readable.


In the previous post we happened upon the idea of separating our logic into different levels - "layers of abstraction" is the popular term. Broad, overarching logic like the GUI, low-level logic like the elements and their precious pixels, and never the twain shall meet.

It turns out that there might be one or two useful levels in between those. The GUI doesn't necessarily care about the elements itself, but it does care about the script window. Does the script window care about the elements? Well, sort of, but it probably needs a way to organize them.

It also turns out that our elements are already organized by z, their front-to-back depth. The GUI does a lot of its processing by depth, in fact.

A concept that could lives on its own and existing code that makes use of the same structure? I feel a refactoring coming on.

Oriental Objections

Our new class, seductively named Layer, will take a bunch of scattered tasks away from the GUI and put them in one place where they all look the same and have the same political opinions and... oh dear, this metaphor was a bad idea.

Let's have a look at what the GUI currently does and how Layer can streamline the process:

  • When elements are created, they're stored in a table - GUI.elms.
  • When the window is opened the elements are also placed in another table and sorted by z. This table is updated on each loop of the GUI.

We can store elements in their Layer and let the GUI concern itself solely with the layer order.

  • On each loop, the GUI uses that sorted table to update the elements and then redraw them.

The GUI can simply tell each Layer to update or redraw, letting the Layer pass that instruction to its child elements. This doesn't save any processing, but it does help with the "a CEO shouldn't be micromanaging the cashiers" issue we discussed prevously. Setting up different aspects of a program to communicate in vague terms - "hey, do your thing and tell me when you're done" - is one of the best ways to keep things from being coupled too tightly together.

  • The GUI has tables tracking which zs need to be redrawn, which are frozen, and which are hidden.

All of these can simply be flags on the Layer itself, and for the most part the GUI doesn't need to be aware of them. When a hidden layer is asked to update itself it can just do nothing.

  • After redrawing the elements for a given z, the GUI blits from that buffer to the main window.

The Layer can handle this internally and blit to whatever buffer the GUI specifies.

A Method To The Madness

Now that we've established what Layer will be responsible for, let's see what that means in terms of class methods.

Layer:new()

Creates our new layer, initializing a few things like its element table.

Layer:addElements(), Layer:removeElements()

In addition to updating the layer's element table, these will also assign element.layer to refer to its parent, allowing us to to move up or down the hierarchy we're creating with ease.

Layer:init(), Layer:update()

Since we're removing the elements from the GUI's supervision, we'll need these so the GUI can prompt the Layer to initialize and update them instead. I expect a lot of the init logic to change in the future, but we'll work with it for now.

Layer:hide(), Layer:show()

Again, since the GUI isn't handling this anymore the Layer will need to expose them itself.

Layer:redraw()

This one can be essentially cut/pasted to its new home, minus a couple of changes to variable names. As above, the GUI can now tell a Layer to redraw itself with a vague hand-wave rather than doing all of the work itself.

Layer:findElementByName()

If it feels like we're getting a little Javascripty here... well, you're right. Since the GUI no longer has a master table of elements, we're going to need some helpers for finding elements when we need them. The GUI will get a matching function that simply goes through all of the layers until it finds a match - the existing GUI.Val("myElement") will borrow that to do its job.

Ch-Ch-Ch-Changes

The addition of Layer necessitates a few modifications elsewhere, the big one being, of course, that GUI.elms is replaced by GUI.Layers. Anything that referred to it will have to be rewired a bit - specifically, elements are no longer created with:

GUI.New("myButton", "Button", 4, 0, 0, 64, 24...

A while back I added an alternate syntax for creating elements in a slightly more readable fashion:

GUI.New("myButton", "Button", {
  z = 4,
  x = 0,
  y = 0,
  w = 64,
  h = 24,
  ...
})

This is now the only way to create elements. Why? Because I said so. And because it's more readable. And because it lets you create elements with whatever arbitrary parameters you want.

With a bit of tweaking to make it friendlier and easier to chain, and combined with the addition of layers, the new element creation process looks roughly like this:

local layer = GUI.createLayer({name = "Layer1", z = 1})
layer:addElements(
  GUI.createElements(
    {
      name = "my_lbl",
      type = "Label",
      x = 256,
      y = 96,
      caption = "Label!"
    },
    {
      name = "my_knob",
      type = "Knob",
      x = 64,
      y = 112,
      w = 48,
      caption = "Volume",
      vals = false,
    },
    ...
  )
)

Pretty slick in my opinion.

Other changes we need to make include:

  • Element.delete() will need to call removeElements to let its parent layer know. You know how parents are... if they don't hear anything, they worry. I'm not just saying that because I have a four-year-old.

  • Elements will no longer have a z property, since they'll be attached to a Layer. The old GUI.redraw_z[self.z] mess is gone (yay!), replaced by self.layer.needsRedraw = true.

  • The Tabs class will need a small rework, as it currently manipulates the GUI.elms_hide table to do its thing. Rather than giving it a table of z values for each tab:

    z_sets = {
    [1] = {2, 3, 4},
    [2] = {2, 5, 6},
    [3] = {2, 7, 8},
    }

    we can give it a table of Layers themselves:

    tab.tabs = {
    {label = "First", layers = { layer1, layer2, layer3 },
    {label = "Second", layers = { layer4, layer5, layer6 },
    }

    and have it call layer:hide() or :show() as needed.

Done!


Next time on Dragonball Z: All these shenanigans have broken some of the code in our test script.