An Introduction to InputSets and InputTables

In this tutorial, we will learn how to use InputSets and InputTables in Quorum. InputSets are used to uniquely identify input, such as keyboard or mouse, and expand existing functionality in the language. These are used in conjunction with InputTables, which associate these inputs with Behaviors we intend to work across a set of items. For example, we could use an InputSet to trigger when the user clicks the mouse on a creature in a game, and then use an InputTable to tell it how to respond to that input.

InputSets have several advantages compared to event listeners, but they do not replace them. Notably, unlike listeners, they can be used to make input act consistently across groups of similar Items by default. For example, buttons in a user interface should have default behaviors, like slightly changing colors when clicked to visually show that it happened.

InputTables have similar advantages. With listeners and the event system, the programmer could make whatever they wished, but input tables make it easier to remap those commands in different ways. For example, they can be used to set multiple inputs to trigger the same Behaviors or as a way to remap keyboard or mouse input across an entire system.

Using the Default InputTable

Let's make a program which will perform a task by default when given a particular kind of input. We could choose any input, but for example, we will create a Behavior that outputs the name of the currently focused Item when the spacebar is pressed. The Behavior we will use is shown in the code block below. We can review how Behaviors work in the Behavior tutorial. We can also review the Focus tutorial.

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Item

class OutputBehavior is Behavior
    action Run(BehaviorEvent event)
        Item item = event:GetItem()

        // There may not be a focused Item. Output something different
        // if there was something focused.
        if item = undefined
            output "No item is in focus."
        else
            output item:GetName() + " is focused."
        end
    end
end

Now we'll create our main class. Inside it, we'll make an InputSet that triggers this Behavior. We'll also want to make a KeyboardEvent, which is used to provide values that represent the keys on the keyboard (e.g., spacebar). The fields will look like this:

InputSet spaceSet
KeyboardEvent keys

Inside our CreateGame action, we need to tell Quorum that we want to use these input sets. To do this, we set our InputSet to accept a particular input from the keyboard using the SetKeyboardInput action. We'll use our keys variable to provide the SPACE constant from the KeyboardEvent class.

After we've set up our InputSet, we need to get the default InputTable from the system. We can retrieve this table using the Game's GetDefaultInputTable action. The default InputTable applies Behaviors whenever input is received in a structured and organized way.

Now that we have our InputTable, we can set it to trigger our Behavior when the spacebar is pressed:

OutputBehavior outputBehavior
defaultTable:Add(spaceSet, outputBehavior)

The default InputTable will now trigger when the spacebar is pressed. In the provided code sample, we've included some Drawables and set them up to allow for Focus in our Game.

use Libraries.Game.Game
use Libraries.Game.InputSet
use Libraries.Interface.Events.KeyboardEvent
use Libraries.Game.InputTable
use Libraries.Game.Graphics.Drawable
use Libraries.Game.Graphics.Color

class Main is Game
    InputSet spaceSet
    KeyboardEvent keys
    Color color
    Drawable square
    Drawable circle

    action Main
        StartGame()
    end

    action CreateGame
        // Set up our InputSet to trigger when the spacebar is pressed.
        spaceSet:SetKeyboardInput(keys:SPACE)

        // Get the default InputTable from the Game.
        InputTable defaultTable = GetDefaultInputTable()

       // Create our Behavior and make it trigger every time the spacebar is pressed.
       OutputBehavior outputBehavior
       defaultTable:Add(spaceSet, outputBehavior)

       square:LoadFilledRectangle(100, 100, color:Orange())
       square:SetName("Square")
       square:SetFocusable(true)
       square:SetPosition(200, 250)
       Add(square)

       circle:LoadFilledCircle(50, color:Purple())
       circle:SetName("Circle")
       circle:SetFocusable(true)
       circle:SetFocusable(true)
       circle:SetPosition(500, 250)
       Add(circle)

       square:SetNextFocus(circle)
       square:SetPreviousFocus(circle)
       circle:SetNextFocus(square)
       circle:SetPreviousFocus(square)
       SetFocus(square)
    end
end

InputTables and Input Groups

The default InputTable can be useful for default behaviors as a whole and, perhaps, even more so for assigning Behaviors to specific kinds of items. The reason this is the case is because different kinds of user interface components, like textboxes or buttons, act in unique ways and therefore might use unique default behaviors.

In this code, we've added a circle and a square, and they're set to cycle the focus between them. Now let's set them to use special input. To start, let's make new Behaviors that we'll use for these Drawables. The first Behavior is listed below. This one will rotate the item by 45 degrees, which we'll use for our square.

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Item
use Libraries.Interface.Item2D

class RotationBehavior is Behavior
    action Run(BehaviorEvent event)
        Item item = event:GetItem()

        // There may not be a focused Item. If not, put something in the console
        // so that we know what happened.
        if item = undefined
            output "Couldn't rotate because no item was focused."
        elseif item is Item2D
            // Cast our item to an Item2D (Drawable is a kind of Item2D)
            // and then rotate it.
            Item2D item2D = cast(Item2D, item)
            item2D:Rotate(45)
            output item2D:GetName() + " set to " + item2D:GetRotation() + " degrees."
        end
    end
end

Our second Behavior is a sort of toggle. Each time it's activated, it will alternate between making the object smaller or larger by using the Scale action. We'll use this one for our circle. The code for this Behavior is listed below:

use Libraries.Interface.Behaviors.Behavior
use Libraries.Interface.Events.BehaviorEvent
use Libraries.Interface.Item
use Libraries.Interface.Item2D

class ScaleBehavior is Behavior

    boolean scaledUp = false

    action Run(BehaviorEvent event)
        Item item = event:GetItem()

        // There may not be a focused Item. If not, put something in the console
        // so that we know what happened.
        if item = undefined
            output "Couldn't scale because no item was focused."
        elseif item is Item2D
            // Cast our item to an Item2D (Drawable is a kind of Item2D)
            // and then scale it.
            Item2D item2D = cast(Item2D, item)
            if scaledUp
                item2D:SetScaleFromCenter(1.0)
            else
                item2D:SetScaleFromCenter(1.5)
            end
            scaledUp = not scaledUp
            output item2D:GetName() + " scaled to " + item2D:GetScaleX() + " times its normal size."
        end
    end
end

Using these two Behaviors, we can specific functionality to specific kinds of items in our user interface. For example, suppose we would like to apply the RotationBehavior only to the square and the ScaleBehavior only to the circle. If this was our goal, we could use the SetInputGroup(text) action from our Drawables. The input groups are available to any Item, and are used to identify what InputTable should be applied to them. For our code, we'll set the following input groups:

square:SetInputGroup("Square")
circle:SetInputGroup("Circle")

Now that we have set the input groups, we can make InputTables that correspond to them. When we create these tables, we'll use the SetIdentifier action in the InputTable. The important part is that we need to match these identifiers to the input groups of the Items we want them to correspond to, like so:

InputTable squareTable
squareTable:SetIdentifier("Square")
InputTable circleTable
circleTable:SetIdentifier("Circle")

Once our tables are created and their identifiers are set, we can add our InputSets and Behaviors to them. To do this, we use the Game's AddInputTable action to add the tables to the Game. Note that we didn't need to do this for the default InputTable because it's already part of the Game, but these new tables must be added in order to tell our interface that we are adding a custom user interface component. When we do so, our main class looks like this:

use Libraries.Game.Game
use Libraries.Game.InputSet
use Libraries.Interface.Events.KeyboardEvent
use Libraries.Game.InputTable
use Libraries.Game.Graphics.Drawable
use Libraries.Game.Graphics.Color

class Main is Game

    InputSet spaceSet
    KeyboardEvent keys

    Color color
    Drawable square
    Drawable circle

    action Main
        StartGame()
    end

    action CreateGame
        // Set up our InputSet to trigger when the spacebar is pressed.
        spaceSet:SetKeyboardInput(keys:SPACE)

        // Get the default InputTable from the Game.
        InputTable defaultTable = GetDefaultInputTable()
        
        // Create our Behavior and make it trigger every time the spacebar is pressed.
        OutputBehavior outputBehavior
        defaultTable:Add(spaceSet, outputBehavior)

        square:LoadFilledRectangle(100, 100, color:Orange())
        square:SetName("Square")
        square:SetFocusable(true)
        square:SetPosition(200, 250)
        Add(square)

        circle:LoadFilledCircle(50, color:Purple())
        circle:SetName("Circle")
        circle:SetFocusable(true)
        circle:SetPosition(500, 250)
        Add(circle)
        
        // Setup our Drawables with the focus system.
        square:SetNextFocus(circle)
        square:SetPreviousFocus(circle)
        circle:SetNextFocus(square)
        circle:SetPreviousFocus(square)
        SetFocus(square)

        // Setup our Drawables' input groups and make InputTables with matching IDs.
        square:SetInputGroup("Square")
        circle:SetInputGroup("Circle")

        InputTable squareTable
        squareTable:SetIdentifier("Square")
        InputTable circleTable
        circleTable:SetIdentifier("Circle")

        // Create our behaviors. In this example, we use the same InputSet from
        // earlier for our square, and we'll use the Enter key for the circle.
        RotationBehavior rotationBehavior
        ScaleBehavior scaleBehavior
        InputSet enterSet
        enterSet:SetKeyboardInput(keys:ENTER)
        squareTable:Add(spaceSet, rotationBehavior)
        circleTable:Add(enterSet, scaleBehavior)
        
        AddInputTable(squareTable)
        AddInputTable(circleTable)
    end
end

In our example, we use the same InputSet that we used previously for our OutputBehavior to trigger our RotationBehavior, while we use a new InputSet using the Enter key to trigger our Scale behavior.

Notice, however, that when our square is focused, the spacebar no longer triggers the OutputBehavior. This is because the square's related InputTable has its own Behavior it triggers on spacebar presses, which takes priority over the default InputTable. For the circle, however, the spacebar still triggers the InputTable, because the circle's table does not intercept the spacebar.

Input Triggers

When using InputTables, we may want to our Behaviors on different conditions. When using InputSets, we have three different "triggers" that we can use to respond to input:

To set an InputSet to trigger on one of these conditions, we use the SetInputTrigger action, providing it one of the above constants within the class, as shown here:

InputSet enterSet
enterSet:SetKeyboardInput(keys:ENTER)
enterSet:SetInputTrigger(enterSet:CONTINUE)

Keyboard Modifiers

When using InputSets on desktop platforms, we may also want to trigger Behaviors only if certain keyboard modifiers are being held down. For example, we may want to do something different if the user presses "space" or "control-space," or if the user is holding down the shift key while clicking on something.

The following keyboard modifiers may be used:

We can add modifiers to an InputSet using the AddModifier action, and remove modifiers with the RemoveModifier action. For example, the below code shows an example of how to set an InputSet to trigger on a control-left-click.

MouseEvent mouse
InputSet controlClick
controlClick:SetMouseInput(mouse:LEFT)
controlClick:AddModifier(controlClick:CONTROL)

Double-Click and EventCounts

While using InputSets, we often trigger Behaviors only when an input occurs multiple times in short succession. This occurs most often with double-clicks, but can appear in other forms of input as well (e.g., double-taps on keyboard keys or a touch screen).

By default, InputSets trigger on the first input, but we can adjust this using the SetEventCount action. For example, the below code can be used to create an InputSet that triggers on a double-left-click:

MouseEvent mouse
InputSet doubleClick
doubleClick:SetMouseInput(mouse:LEFT)
doubleClick:SetEventCount(2)

Next Tutorial

In the next tutorial, we will discuss Layouts, which describes how to use Layouts.