Skip to content

Tutorial

Matthew Shapiro edited this page Jan 14, 2024 · 12 revisions

This guide will walk you through the process of getting Gum up and running and adding a new screen to an existing MeadowGum application. We will add a screen that creates a NES style name entry window, in the same style of:

Ultima_Quest_of_the_Avatar_-NES-_Name_Entry

At the end of this series you should have an understanding of

  • Installing and running the Gum tool
  • The basic concepts of Gum and how to use them in the Meadow context
  • How to create a functional screen that can be used on a Project Lab v3.

This tutorial is meant to give a quick understanding of the gum tool, and more complete information is available in the official Gum docs.

Note: The gum tool only supports windows at this time.

Initial Setup

Code

In this tutorial we will modify the demo project that's included in this project's repository. So the first thing we need to do is clone the code from this repository.

Note: it is important to check out the repository with submodules. Likewise, any time the repository is pulled it is important that submodules are updated as part of the pull. This is because we reference Gum via git submodules at the current time.

The MeadowGum solution has the following notable projects:

  • MeadowGum.Core - Main library that all MeadowGum projects reference
  • MeadowGum.ScratchPad.Common - Shared library for the demo logic. Most UI elements and logic are defined in here
  • MeadowGum.ScratchPad.Monogame - Monogame based executable project to run the demo project on a PC. Used for quick testing and iteration.
  • MeadowGum.ScratchPad.ProjectLab3 - Project which deploys and executes the demo project onto a Meadow ProjectLab v3.

You can verify everything works by running the MeadowGum.ScratchPad.Monogame project on your PC. This application uses the arrow keys for navigation and interactivity.

Gum Tool

To run the gum tool, XNA 4.0 needs to be installed. It can be downloaded directly from Microsoft's download page.

Once installed the Gum tool can be run from prebuilt binaries downloaded which can be downloaded from the Gum docs site.

Exploring the Gum Project

Open the Gum tool and go to File -> Load Project. Select the MeadowGum/MeadowGum.ScratchPad.Common/Gumlayouts/layouts.gumx. This file contains all the defined resources and setting for our demo project. In the tree view you will see all elements of the current project.

image

These element categories are:

  • Standard - A set of standard UI elements that are the basic building blocks of user interfaces.
  • Only Text, Sprite, Container, and ColoredRectangle standard elements are supported in a meadow context
  • Components - A predefined configuration of one or more standard elements. This provides a re-usable set of elements that can be replicated in one or more screens.
  • Screens - A screen is a holistic (and usually independent) user interface.

Clicking on any screen, component, or standard element will select that item. Upon selection, the UI will update to show a preview of that element, any defined states, and that element's properties. For example:

image

Note: Text sizing is not accurate in the preview pane from a Meadow perspective. This is because Gum relies on normal computer fonts for text and the Meadow uses custom-defined monospace fonts that are non-resizable. So while the Gum tool shows a mostly accurate representation of how the screen will look, it will not be exact when text elements are included.

Adding the keyboard screen

We can add our new screen by right clicking Screen in the tree view and selecting Add Screen. In the popup give it the name NameEntry and click Ok. This automatically adds an empty screen and selects it

image

Next we can add the demo's standard background to the screen by dragging the background component from the tree view into the new screen.

tutorial-01

Lets provide some text into our screen so users know which screen they are on, so do the same with the ScreenLabel component. This component is a text label that's automatically set up to be positioned in the top right corner, in the same way other screen labels appear. We can then change the Text property of the ScreenLabelInstance that was created to be the text we want to display.

tutorial-02

Code Generation

Underneath the preview section is a set of tabs, one of which is labelled Code.

image

This tab has two purposes:

  1. It allows previewing the code that was generated by the automatic code generation for the selected item.
  2. Setting the project wide code generation settings, including when to perform code generation (manual vs automatically).

Since this project is set to generate code automatically on property change, this means it should have automatically generated the code for our screen. If you switch back to your IDE and look at the MeadowGum.ScratchPad.Common/Screens folder you should now see two new files, NameEntryRuntime.cs and NameEntryRuntime.Generated.cs. These files contain the code that will contain UI elements and custom logic for them.

image

Opening NameEntryRuntime.Generated.cs file will show the exact same code the Gum tool showed in the preview. Specifically, it instantiates the background and screen label components, applies their default values (including the name we gave the screen label), and a declaration we can use to add custom initialization logic after all UI elements have been set up. This file should not be manually edited, as the Gum tool will replace its contents when any property of any UI element in the screen is changed.

Likewise the NameEntryRuntime.cs was created but will never be touched again by the Gum tool. This file is where we will write all custom logic for our interface.

You can see that the NameEntryRuntime class inherits from MeadowGumScreen. This is a base class for the MeadowGum framework which handles rendering. To fix the compile errors and make our screen usable, we need to implement the public override Task<MeadowGumScreen?> RunAsync(CancellationToken cancellationToken) abstract method.

This function will be executed by the MeadowGumScreenManager. When this method returns, the return value is either the next MeadowGumScreen implementation the framework should display, or null to say that the MeadowGumScreenManager should exit (normally meaning the application is done).

For now, we don't want any functionality except to display the screen and sit there. So we can implement this method with the following code:

    public override async Task<MeadowGumScreen?> RunAsync(CancellationToken cancellationToken)
    {
        Render();

        while (true)
        {
            await Task.Delay(1000, cancellationToken);
        }
    }

Adding to the main menu

While we've added logic to render the screen, we haven't given the application any way to actually activate our screen.

To do that we need to add a new button to the main menu. In the gum tool, expand the Screens -> MainMenu tree view item, then drag the Components -> SpriteButton component into the ButtonContainer.

tutorial-03

The ButtonContainer is just a standard container element that allows vertically stacking UI elements with spacing in between. You may notice that the button is left aligned. We need to fix that by changing two properties:

  • X Units should be set to Pixels from Right
    • This tell Gum we want the origin of this button to be X units from the right of the container
  • X Origin should be set to Right
    • This tells Gum we want the origin of the button to be the right most part of the component.

tutorial-04

Now that we have a button, we need to update the label of the button and give the button's instance a suitable name. The Name property is important, as the element will contain that same name in the generated C# code.

tutorial-05

Finally, we need to update the main menu screen's UI logic to transition to this screen. In the MeadowGum.ScratchPad.Common/Screens/MainMenuRuntime.cs file we can add the following code inside RunAsync():

if (_buttons[_selectedIndex] == NameEntryButton)
{
    return new SplashScreenRuntime
    {
        TitleText = "Name Entry Demo",
        DescriptionText = "Tutorial for creating a name entry screen",
        NextScreen = () => new NameEntryRuntime(),
    };
}

This code checks if the currently selected button is the NameEntryButton we created, and if so it returns an instance of the splash screen with information about the screen we will transition to. The splash screen takes a function that will create our NameEntryRuntime and automatically execute it.

We can verify this works by running the MeadowGum.ScratchPad.Monogame project.

tutorial-06

Creating keyboard keys

We need 27 keys to make this screen work, one for each letter and one for backspace. When a key is selected it should be visually distinct from non-selected keys. This is a perfect use case for components, as we can create a single component that can be used for each individual instance of a key.

A component can be created by right clicking Components, selecting New component, and giving that component a name. Lets call our component KeyCap.

tutorial-07

We want to add a few elements to our component and set their respective properties

  • A white ColoredRectangle named Border to represent the outer edge of our key
    • Set Name to Border
    • Set X Units to Pixels from Center
    • Set X Origin to Center
    • Set Y Units to Pixels from Center
    • Set Y Origin to Center
  • A black ColoredRectangle named Background to represent the background of our key
    • Set Name to Background
    • Set Red, Green, and Black to 0
    • Set X Units to Pixels from Center
    • Set X Origin to Center
    • Set Y Units to Pixels from Center
    • Set Y Origin to Center
  • A TextElement named Label which will hold the key's value
    • Set Name to Label
    • Set X Units to Pixels from Center
    • Set X Origin to Center
    • Set Y Units to Pixels from Center
    • Set Y Origin to Center

tutorial-08

Of course, this requires more tweaking to get a look we want. One thing you may notice is that text isn't actually centered even though we told it to. This is because by default text elements have a default size of 100x50 pixels, and is not based on the size of the text contents. This is useful if you want a word wrapped text area (like on the splash screen), but is not useful for our purposes.

So we can fix that by changing Width Units and Height Units to Relative To Container. This says "the size of the element is the size of all it's children plus the specified width and height values". For text elements, its child is the text content itself. Then we set width and height to 0 to make it exactly the size of the text.

tutorial-09

Now we have centered text!

However, we now have a Background that's statically sized, and if we type too much text in the label then it will overflow. Instead we want the background to be a bit wider than the label, but be relative to its size. We can do that by dragging the label onto the background, so the label is a child of the background, and then use the same Relative To Container sizing. Setting Width to 4 and Height to 4 means the background will be 4 pixels wider and 4 pixels taller than the size of the label's text (and since everything is centered, it will be 2 pixels padding on each horizontal side, and 2 pixels padding on the vertical side).

tutorial-10

We can repeat the process for the border, making the border have a 2x2 padding on either side of the background.

tutorial-11

The only thing left is to also make the KeyCap component itself the same size as its children.

tutorial-12

So now our KeyCap will automatically size itself based on what text is inside the label. However, we do not have an easy way to customize the text inside each key cap on a per instance basis. To do that we have to expose the label's Text property to the root of the component. This can be done by going to the Label instance we created, scrolling to the Text property, right clicking and selected Expose Variable. Once that's done it will show under the variables property of the component. Changing the LabelText variable will change the label's Text property.

tutorial-13

Now, if you go back to your C# IDE, you should see two new code files in the MeadowGum.ScratchPad.Common project, Components/KeyCapRuntime.cs and Components/KeyCapRuntime.Generated.cs.

Open the KeyCapRuntime.cs file and change the inherited class from BindableGraphicalUiElement to GraphicalUiElement This is a bug in the Gum tool that will be addressed and won't be needed sometime in the future). You'll now see that just like our screen, we've had a non-generated partial class implementation created for us that we are free to write custom logic into, as well as a .Generated.cs file that has all the settings we set up in the Gum Tool.

Adding keys to the screen

So now we have a re-usable key that we can add to our screen. Let's add it to our screen. We will need 27 of these keys. The easiest way to organize this is in a grid of keys.

So lets go back to our NameEntry screen and create a container to hold our keys. We do that by dragging the Standard -> Container element up into our screen in the tree view, name it KeyContainer, and position it to be aligned with the right and bottom of the screen. We will also want to give this container a width of 225 and height of 150 to give us enough room for the keys.

tutorial-16

Containers can have different layouts for its children. The default layout is Regular, which means every item is positioned based on it's own x/y position relative to the container. However, we can tell this to space the container's children into a grid like layout. So select Auto horizontal Grid and we'll give it 6 columns and 5 rows.

tutorial-17

Now we need to add the keys to the container. To do that, drag the Component -> KeyCap tree view item up into the KeyContainer. Select the KeyCap tree view entry, hit ctrl+c to copy it, then press ctrl+v 26 times to quickly create 27 key caps.

tutorial-18

Now for each key, we need to set the key to the letter it will correspond with (except the top right most button, we want to say Del). This is done by selecting each KeyCap instance itself and changing the Label Text exposed variable. Unfortunately, we'll need to do this one at a time.

tutorial-19

At this point you have 27 buttons for the whole alphabet, plus a Del key. You may notice that the keys are touching each other in the Gum's rendering of the screen, but if you run the monogame project you'll see that's just an artifact of Gum thinking the text is bigger than it really is.

image

Selecting Keys

So all keys are now on the screen, but we can't pick any keys. First we need a way to visually determine which key is selected. This can be done via states and categories.

Select the Components/KeyCap tree view item, look for the States tab in the middle of the UI, right click and select Add Category. We want to add a new category named Selection. Next right click on the Selection state category, click Add State, and name it Selected. Repeat the process for an Unselected state.

tutorial-20

Notice that the Variables tab in the bottom center now has a yellow bar that says Editing state Selection/Unselected. This means that any changes to the Variables you make will only affect the the component when it's in the current state category.

We can leave the Unselected state alone, but lets change the border to have a green background when it's selected. To do that, click the Selection/Selected state, click the KeyCap/Border tree view item, scroll down the variables and change Red and Blue to 0.

tutorial-21

Now when you click between the Selected and Unselected state, you can see that the color changes as set.

Back in your IDE, open the Components/KeyCapRuntime.Generated.cs file. Towards the top you'll see that Gum automatically generated a Selection enum with Selected and Unselected as it's variants. Likewise, it created a SelectionState property that changes variables based on what value was passed into that property.

So let's take advantage of that by opening the KeyCapRuntime.cs (non-generated) file and add the following methods:

public void Select()
{
    SelectionState = Selection.Selected;
}

public void Deselect()
{
    SelectionState = Selection.Unselected;
}

So now we can trigger these states on any of our KeyCap instances via the Select() and Deselect() method calls.

Now open up the Screens/NameEntryRuntime.cs file. We need two fields, a list of all key caps and the index of the currently selected key:

private readonly List<KeyCapRuntime> _keys = new();
private int _selectedIndex = 0;

Next we need to add all of our key caps into the list, and mark the first key as selected. This has to be done in the CustomInitialize() method so it's guaranteed to happen after all UI elements have been configured.

partial void CustomInitialize()
{
    _keys.AddRange(KeyContainer.Children.OfType<KeyCapRuntime>());
    _keys[0].Select();
}

If you run the application you should see the A key glowing green!

image

First let's create a method in the NameEntryRuntime class to handle button presses.

    private void HandleButtonEvent(InputManager.ButtonEvent buttonEvent)
    {
        _keys[_selectedIndex].Deselect();

        if (buttonEvent.Name == ButtonNames.Left && buttonEvent.State == InputManager.ButtonState.Clicked)
        {
            _selectedIndex--;
            if (_selectedIndex < 0)
            {
                _selectedIndex = _keys.Count - 1;
            }
        }
        else if (buttonEvent.Name == ButtonNames.Right && buttonEvent.State == InputManager.ButtonState.Clicked)
        {
            _selectedIndex++;
            if (_selectedIndex >= _keys.Count)
            {
                _selectedIndex = 0;
            }
        }
        
        _keys[_selectedIndex].Select();
    }

Originally we created our RunAsync() method to render only once, and then just await forever. However, we need to change this check for button presses and then Render() after each key press. So change the RunAsync() method can be:

    public override async Task<MeadowGumScreen?> RunAsync(CancellationToken cancellationToken)
    {
        while (true)
        {
            Render();
            var buttonEvent = await InputManager.Instance.WaitForNextButtonAsync(cancellationToken);
            if (buttonEvent != null)
            {
                HandleButtonEvent(buttonEvent);
            }
        }
    }

Now if you run the demo application you will be able to use the left and right keyboard keys to select different keys!

tutorial-22

Deploy To The Meadow

Now let's see this running on the Meadow. Build the MeadowGum.ScratchPad.ProjectLab3 build in release mode and deploy it out to your Project Lab.

After the deployment, you should be able to use the ProjectLab's buttons to open the screen you created and navigate through the buttons.