View Single Post
Old 05-21-2016, 09:21 AM   #57
Human being with feelings
Lokasenna's Avatar
Join Date: Sep 2008
Location: Calgary, AB, Canada
Posts: 6,346

11. Example classes - TxtBox, Sldr, and Knb

Note: This lesson refers to the LS GUI library provided in part 6. If you haven't downloaded those files yet, see this post: 6. Introducing the LS GUI library


Typing. Everyone does it. Your script might want to do it too. Here's how.

(Cheers to schwa for his text box example, which this class and methods were largely copied from)

As we saw last time, GUI.Update() has each element check to see if it's been typed in. This requires a) the user to have pressed a key and b) the element to be in focus. focus is set when the element detects a left-click, and here it will activate the TxtBox for typing.

In the :onmousedown() method, we place the typing caret (the little blinking line) using another method that's exclusive to TxtBox - :getcaret():
self.caret = self:getcaret()
This is done by looping through the textbox's contents, one character at a time, and seeing if the x position of that character matches the position of the mouse cursor.
	for i = 1, len do
		w = gfx.measurestr(string.sub(self.retval, 1, i))
		if GUI.mouse.x < (self.x + self.pad + w) then return i - 1 end
As you would expect, :ondrag() lets you select part of the text and :ondoubleclick() selects all of it. The selection is stored as a position relative to the caret.

:ontype() is, surprise surprise, significantly larger than other methods. However, after deleting any selected text, the bulk of the function is just one big "if/then, elseif, elseif, elseif..." statement because we have to tell Lua what to do for each keyboard key. For this example I've programmed in the arrow keys, backspace, and delete.

All that's left, then, is to draw everything. The frame and text are drawn as you would expect, and for selected text we draw a rectangle on top and then redraw that portion of the text in a different color. If the box is focused, we also need to draw the caret; to make it blink, the text box uses a counter to keep track of how many times the Main() loop runs and only draws the caret half of the time.

self.blink = (self.blink + 1) % 16
For those not familiar, the % symbol is for the mathematical operation "modulo", which gives you the remainder after subtracting as many 16s (in this case) as you can from the original number. Stated another way, this line of code just adds 1 to the current total and loops back to 0 when it hits 16, over and over.


Now that we're all experts on clicking and typing, how about dragging?

Because the Sldr class also has a few new parameters, we'll start right at the beginning:
function Sldr:new(x, y, w, caption, min, max, steps, default)
The first thing you'll notice is that it's missing a parameter for height. Why? Because width as all we care about; we're hardcoding the size of the handle, so it wouldn't make sense to let users adjust the height of the "track". You could rewrite it to include height if you wanted to, you weirdo.

We've also got the slider's minimum and maximum values, how many steps to include between them, and a default step to start the slider on.

Sldr only deals with steps when it's communicating with the user, with all of the internal stuff handled as decimal values between 0 and 1, so we need to convert the :new() parameters from steps to decimals:

	sldr.curval = sldr.curstep / steps
	sldr.retval = ((max - min) / steps) * sldr.curstep + min
Notice that curval is our 0-1 value, and retval is adjusted for the min, max, and step parameters.

The :onmousedown() method is very similar to TxtBox's - it checks the position of the mouse cursor within the slider and sets the slider to the closest value.

:ondrag(), however, takes a little more effort, as it needs to know a) how far it's been dragged and b) if the Ctrl key is being held down. Ctrl is a fairly common modifier for sliders and knobs that allows for finer control.

To track how far the slider has been dragged, GUI.Update() stores the mouse position from the previous Update loop in GUI.mouse.lx and ly.

Back to Sldr:ondrag(), you can see that we're just comparing the current position of the mouse to where it was in the last loop, and multiplying the difference by a constant value. To allow Ctrl as a modifier, it's just a matter of choosing between two different constants based on the Ctrl state.

Both methods also check to make sure the new slider value is still inside the slider, and then they round the value to the nearest step.

As far as drawing the Sldr there isn't too much going on that we haven't previously covered. A roundrect() provides the track, for the handle, and then the caption and current value.


Knobs are pretty much identical to sliders. :ondrag() looks at changes in the y value of the mouse rather than x, and knobs turn instead of moving, so each step is an angle instead of a horizontal offset. In Knb:new() we figure out the size of each step:
	-- Determine the step angle
	knb.stepangle = (3 / 2) / knb.steps
This part might be really upsetting for anyone who didn't enjoy high school math class. One word for you: radians. Here's a brief introduction for those who need to brush up.

*pause for the screaming and rioting to settle down*

Radians are a tidier way of measuring things on a circle than degrees, and ReaScript has a few functions that require you to use radians as well. In short, 2pi radians is a whole circle - 360 degrees, and pi radians is 180. For a knob, the minimum and maximum positions are often at the southwest and southeast positions, equal to three quarters of the circle or 3/2pi.

Moving on to :draw() we first convert the x,y,w values to things that can use - coordinates for the center of the circle, and a radius. Next we figure out the current angle (in radians) based on which step the knob is on.

	local curangle = (-5 / 4) + (curstep * stepangle)
Things worth noting:

- Radian math uses east as its 0. That -5/4 is just an offset so that our minimum is where we want it to be.

- I've specifically written the code to avoid using pi directly. It's not necessary, but I felt that this way was neater and more readable. It might just be me.

- :draw() makes good use of another helper function, polar2cart(), which takes a position in radians, along with your circle's center coordinates, and returns the equivalent x,y coordinates. This is also where pi gets factored in.

local x, y = polar2cart(angle, r, o.x, o.y)
After the tick marks and values are drawn, it's time for a new function. Oh boy oh boy oh boy oh boy!

gfx.triangle(x1, y1, x2, y2, x3, y3)

I really shouldn't have to explain how that one works.

The built-in triangle function will only draw them filled in, so our library includes yet another wrapper function to provide us with a fill option - GUI.triangle(). It adds an additional parameter at the beginning, a 1 or 0 to determine the fill state.

	GUI.triangle(0, Ax, Ay, Bx, By, Cx, Cy)
The knob itself is made of a circle and a triangle smushed together. The following block of code determines three points for the triangle - one sticking out of the circle toward the current value, and two more 90 degrees (1/2pi radians) to either side of it, just inside the circle. Put them together, throw in a shadow, and you've got yourself on sexy knob.

As with Sldr, Knb works with the steps as values from 0 to 1 and then converts them to the correct format when drawing text and returning a value.
Lokasenna is offline   Reply With Quote