Chapter 2 - User input
In this chapter we look at how we can receive and process user input to control our prompter.
Listen for events from keyboard and mouse
Now it’s time get to listen for events. This is the heart of the application. As mentioned above, there are quite a few inputs here, with the various keys and also the use of the mouse. In this application, these can mutually impact each other. For example, if textWdith
increases, more words can be shown per line since there is now space. But if fontSize
increases, each word requires more space and fewer words can be shown. Luckily for us Gio takes care of all of the underlying mechanics, our job is the keep track of the required state variables used to define the visualisation.
As before the switch statement uses type assertion, e.(type)
to deterimine what just happened:
// listen for events in the window.
for e := range w.Events() {
// Detect what type of event
switch e := e.(type) {
// A keypress?
case key.Event:
// Update and store state for size, width and positioning
// A mouse event?
case pointer.Event:
// Update and store positioning state
// A re-render request?
case system.FrameEvent:
// Layout the speech as a list
// Shutdown?
case system.DestroyEvent:
// Break out and end
}
}
The two new events here are:
key.Event
- Was a key just pressed?pointer.Event
- Was a mouse or trackpad just used?
So let’s go through those in more detail:
key.Event
If a key is pressed, Gio receives it as a key.Event. As we see from the docs, the Event is a struct with three variables, Name
, Modifiers
and State
:
type Event struct {
// Name of the key. For letters, the upper case form is used, via
// unicode.ToUpper. The shift modifier is taken into account, all other
// modifiers are ignored. For example, the "shift-1" and "ctrl-shift-1"
// combinations both give the Name "!" with the US keyboard layout.
Name string
// Modifiers is the set of active modifiers when the key was pressed.
Modifiers Modifiers
// State is the state of the key when the event was fired.
State State
}
Name
is simply the letter pressed, or special keys such askey.NameUpArrow
andkey.NameSpace
Modifiers
are keys likekey.ModShift
orkey.ModCommand
, listede here. Note the comment on how Shift is taken into account, but not others, which can be worth knowing about.State
can be either Press or Release, if the distinction is needed
Ok, that gives us something to work with. Once a key is pressed, this will help us detect which key it was, and weither a modifier like Shift is pressed. Here’s the code for this section:
// A keypress?
case key.Event:
if e.State == key.Press {
// To set increment
var stepSize int = 1
if e.Modifiers == key.ModShift {
stepSize = 10
}
// To scroll text down
if e.Name == key.NameDownArrow || e.Name == "J" {
scrollY = scrollY + stepSize*4
}
// To scroll text up
if e.Name == key.NameUpArrow || e.Name == "K" {
scrollY = scrollY - stepSize*4
if scrollY < 0 {
scrollY = 0
}
}
// To turn on/off autoscroll, and set the scrollspeed
if e.Name == key.NameSpace {
autoscroll = !autoscroll
if autospeed == 0 {
autoscroll = true
autospeed++
}
}
// Faster scrollspeed
if e.Name == "F" {
autoscroll = true
autospeed++
}
// Slower scrollspeed
if e.Name == "S" {
if autospeed > 0 {
autospeed--
}
}
// Set Wider space for text to be displayed
if e.Name == "W" {
textWidth = textWidth + stepSize*10
}
// Set Narrower space for text to be displayed
if e.Name == "N" {
textWidth = textWidth - stepSize*10
}
// To increase the fontsize
if e.Name == "+" {
fontSize = fontSize + stepSize
}
// To decrease the fontsize
if e.Name == "-" {
fontSize = fontSize - stepSize
}
// Move the focusBar Up
if e.Name == "U" {
focusBarY = focusBarY - stepSize
}
// Move the focusBar Down
if e.Name == "D" {
focusBarY = focusBarY + stepSize
}
// Force re-rendering to use the new states set above
w.Invalidate()
}
With the expception of stepSize
all these variables are explained earlier. The role of stepSize
is to control how large the change to the other parameters will be. Should a scroll be long or short? Should the focus bar move by lot or a little? Should text size adjustments be considerable or minor? Should … you get it.
The point is that for a user it can sometimes be important to quickly navigate or adjust quite quickly, and thereafter finetune to perfection. Therefor it’s useful to define a variable that controls the rate of change. This defaults to 1, but when Shift
is pressed it increases to 10. Why those value? Well, it worked well in my experimentation. Try it out.
For all the other keypresses, the code adjusts one or two state variables. These are all used later when rendering the actual frame. I went a bit back and forth on the logic around adjusting speed, but conlcuded that if you ask for F
aster scrolling, that should start up the autoscroll if it wasn’t running already. Conversely, if speed is 0 and the user presses Space
to start the scroll, speed must increase. Negative speed is avoided, although it was fun times before I nerfed it.
The point is that for interacting behaviour, it makes sense to experiemnt and think through how the various state variables should be tuned in relation to each other. Keeping it all togehter in this input section makes the code easier to grasp than if these states were handled in various other parts of the program.
At the end we call w.Invalidate()
, forcing the program to re-render so that any new state information is take into account at once. Try commenting this out and re-run. What happens now, and why?
With this in place, here’s an example of how it looks to change fontsize:
pointer.Event
If the mouse is used, Gio receives it as a pointer.Event. That can be any type, such as movement, scrolling or clicking. Once we detect with case pointer.Event:
it is up to us to define what to do with it.
From pointer.Event we learn that the pointer event is quite a rich struct:
type Event struct {
Type Type
Source Source
// PointerID is the id for the pointer and can be used
// to track a particular pointer from Press to
// Release or Cancel.
PointerID ID
// Priority is the priority of the receiving handler
// for this event.
Priority Priority
// Time is when the event was received. The
// timestamp is relative to an undefined base.
Time time.Duration
// Buttons are the set of pressed mouse buttons for this event.
Buttons Buttons
// Position is the position of the event, relative to
// the current transformation, as set by op.TransformOp.
Position image.Point
// Scroll is the scroll amount, if any.
Scroll image.Point
// Modifiers is the set of active modifiers when
// the mouse button was pressed.
Modifiers key.Modifiers
}
What we need here are the two bottom entries, Scroll
and Modifiers
. The former returns a Point
, which is simply a set of X and Y variables that indicate how far the user scrolled in those directions:
type Point struct {
X, Y float32
}
With a scroll-wheel on a mouse it’s always Y only and often in fixed clicking amounts. On a laptop trackpad however it can often be both, and with various amounts.
Modifiers
are just as for the key.Event
a helper to indicate if Shift
or Alt
or any of those are pressed when the mouse event occurs. We’ll continute to listen for the former of those. Like this:
// A mouse event?
case pointer.Event:
if e.Type == pointer.Scroll {
var stepSize int = 1
if e.Modifiers == key.ModShift {
stepSize = 3
}
// By how much should the user scroll this time?
thisScroll := int(e.Scroll.Y)
// Increment scrollY with that distance
scrollY = scrollY + thisScroll*stepSize
if scrollY < 0 {
scrollY = 0
}
w.Invalidate()
}
As with keys we listen for certain events, in this case only the pointer.Scroll
. We want to scroll faster if Shift
is pressed, but the stepSize of 10 from key.Event
proved excessive. So we’re content by increasing it by x3 this time.
After some manipulations, the Y value of a scroll is added to the state variable scrollY
which indicates how far down into the speech we have reached. To reduce confusion we disallow scrolling to before the start by limiting scrollY
to minimum 0
And just as for key.Event
we end by invalidating the frame. Show it to me!