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
z
s 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 callremoveElements
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 oldGUI.redraw_z[self.z]
mess is gone (yay!), replaced byself.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 ofz
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.