Skip to content

Windows and widgets

Plugins show things with three kinds of UI, all built from the game's own widgets:

Made with Good for
Footer button UI::AddFooterButton("label") An entry point in every menu: the bar at the bottom of the main menu, the in-race menus and the track editor.
Panel UI::CreatePanel() A few lines and buttons that open above the footer.
Window UI::CreateWindow() Anything else: overlays, menus, controls.

Build everything once in Main() and keep the handles in global variables. In Update(), read what the player did and change what's shown.

Placing a window

A window is placed by three things:

  • Anchor: a point on the screen, as fractions. (0, 0) is the top left, (1, 1) the bottom right.
  • Pivot: the point of the window that sits on the anchor, also as fractions.
  • Offset: pixels to move it from there.
// Top right corner, 40 pixels in from each edge
window.SetAnchor(1, 0);
window.SetPivot(1, 0);
window.SetOffset(-40, 40);
// Centred on screen
window.SetAnchor(0.5f, 0.5f);
window.SetPivot(0.5f, 0.5f);

A new window sits centred at the bottom of the screen, 40 pixels up.

Rows

Widgets go left to right in rows. NewRow() starts the next row. AddSpace(0) takes up the leftover room, pushing what comes after it to the right:

window.AddText("Show timer", 18);
window.AddSpace(0);
UI::Button@ toggle = window.AddButton("on");
window.NewRow();
window.AddText("restarts 12", 18);

A hidden widget (widget.visible = false) takes no space, so its row closes up.

text.SetFill(true) does the same pushing from a text itself, which suits a list of label and value rows that can each be hidden: the labels line up on the left, the values on the right, and a row with every widget hidden closes up (a space has no visible, so it would hold its row open). SetGapBefore sets the gap before any widget in a row, and window.SetPadding and window.SetRowGap set the space inside the window's edges and between rows. Text can also be drawn in one of the game's fonts with text.SetFont("/Game/UI/Fonts/CocogoosePro.CocogoosePro"). These need host 0.23.6:

window.SetPadding(14, 8);
window.SetRowGap(0);
UI::Text@ label = window.AddText("TOTAL", 12);
label.SetFont("/Game/UI/Fonts/CocogoosePro.CocogoosePro");
label.SetFill(true);
window.AddText("1:42:17", 15).SetGapBefore(18);

From host 0.25.0, window.SetFont(font, typeface) gives the whole window a font (each widget's own SetFont still wins), SetFont takes a typeface such as "Regular" or "Semilight", and buttons and dropdowns can take the game's look: button.SetTextColor, button.SetCornerRadius (a pill or, with an icon button's SetSize, a round button) and dropdown.SetStyle for the box, the list and the hover colours.

window.SetFont("/Game/UI/Fonts/CocogoosePro.CocogoosePro", "Regular");
UI::Button@ play = window.AddIconButton("play");
play.SetSize(40, 40);
play.SetCornerRadius(20);
play.SetBackground(0.434f, 0.839f, 0, 1);
play.SetTextColor(0, 0, 0, 1);

Placed items

Rows suit forms. For anything drawn to measure (bars, charts, a timeline), place rectangles and text at positions of their own, in window units from the window's top left inside its padding. They stay where they're put and can be moved, resized and recoloured every frame without rebuilding the window:

UI::Rect@ bar = window.AddRect(20, 40, 0, 12);         // x, y, width, height
bar.SetColor(0.3f, 0.8f, 0.4f, 1);
UI::Text@ label = window.AddTextAt("", 13, 20, 58);    // text, size, x, y

// every frame
bar.SetRect(20, 40, 300 * progress, 12);
label.text = int(progress * 100) + "%";

AddSlider(0) makes a slider that fills what's left of its row. window.SetCornerRadius(10) rounds a window's corners.

UI::ScreenSize gives the screen's size in window units. The game's footer bar (volume, the plugin buttons, Discord) sits along the bottom; UI::FooterHeight says how tall it is, so a window at the bottom can sit just above it:

float w, h;
if (UI::ScreenSize(w, h))
    dock.SetRect(0, h - UI::FooterHeight() - 200, w, 200);

Hud::HideGame(true) hides the game's race UI for a clear view of the track, and keeps the footer.

Reading what the player did

Buttons, dropdowns, text boxes and tick boxes report events with functions that return true once: reading them clears them. Check each one in a single place, every frame:

void Update(float dt)
{
    if (resetButton.Clicked())      // a click
        seconds = 0;
    if (speedBox.Changed())         // the player picked an option
        speed = speedBox.selected;
    if (nameInput.Submitted())      // Enter in a text box
        name = nameInput.text;
    if (showBox.Changed())          // ticked or unticked
        timerWindow.visible = showBox.checked;
}

Setting a widget's value from your code (speedBox.selected = 2, showBox.checked = true) doesn't count as a change.

The mouse cursor

Clicks only reach your widgets while the cursor is on screen. The game shows it in its menus, including the pause menu. For a window meant to be clicked during a race, ask for the cursor while it's open:

window.visible = open;
UI::SetCursorVisible(open);

UI::CursorShown() tells you whether the cursor is on screen for any reason. It's handy for showing buttons only when they can be clicked.

Movable windows

Set movable after placing the window, and the player can drag it whenever the cursor is on screen. Where they leave it is saved, and reset position on the plugin's settings page puts it back where you placed it.

window.SetOffset(40, 40);
window.movable = true;

For something bigger, like the plugin manager's own menu, a window can have a header (rows shown above every view, for tabs), several views (only one shows at a time) and cards (rows grouped in a rounded box):

window.SetScreenSize(0.6f, 0.7f);           // 60% by 70% of the screen
window.SetBlocksClicks(true);               // clicks never reach the game underneath
window.zOrder = 500;                        // in front of other windows (default 100)
window.SetCardBackground(0.012f, 0.014f, 0.019f, 1);

window.StartHeader();                       // the tabs, shown with every view
UI::Button@ statsTab = window.AddButton("stats");
UI::Button@ optionsTab = window.AddButton("options");

int statsView = window.StartView();         // the header ends here
window.StartCard();
window.AddText("Best time", 19);
window.AddSpace(0);
window.AddText("0:42.17", 19);
window.EndCard();

int optionsView = window.StartView();
window.StartCard();
window.AddText("options go here", 19);
window.EndCard();
window.ShowView(statsView);

// in Update:
if (optionsTab.Clicked())
    window.ShowView(optionsView);

ClearView(n) empties a view so you can fill it again, for example when a list changes. A window can also have a sidebar (StartSidebar(width), then StartMain()) for navigation down the left instead of tabs.

Colours

Colours are red, green, blue and opacity from 0 to 1, in linear values: the game brightens them on screen, so a linear 0.05 shows as roughly 0.25. To match a colour picked on screen (0 to 1 per channel, c), use c / 12.92 for c up to 0.04, else ((c + 0.055) / 1.055) ^ 2.4. A near-black window background is around 0.005.

Text boxes and typing

While the player types in one of your text boxes, the keys go to the box. The game doesn't get them, and Input::Pressed reports nothing. So a hotkey in your plugin never fires by accident while someone types.

The game's own text boxes (the map name in the track editor, the transform boxes, a search box) are different: check Editor::Typing() before acting on a key there.

if (Input::Pressed(Input::G) && !Editor::Typing())
    GroupSelection();

A section in the track editor

DockInEditorDetails() turns a window into a section of the track editor's details panel, shown under the game's transform and paint sections while pieces are selected:

@section = UI::CreateWindow();
section.DockInEditorDetails();
section.SetBackground(0, 0, 0, 0);          // blend in with the panel
section.AddText("my tools", 18);

The track editor's toolbar and key list

Two parts of the editor's own UI take plugin additions, drawn in the game's style:

  • A toolbar button with a dropdown, after the world/local toggle (the globe), like the snapping buttons: Editor::AddToolbarChoice(icon, options, selected). Read the choice with Editor::ToolbarChoice(id).
  • A row in the key list (the panel the info button opens): Editor::AddHotkey(icon, label), or with a second image for "modifier + key". The list keeps its size and scrolls when rows are added. Handling the key is up to you.

Icons are game textures (/Game/..., for example /Game/Art/UI/Textures/KeyboardMouse/keyboard_alt.keyboard_alt) or PNGs in your plugin's folder. Both go away when your plugin stops.

int rotateChoice = Editor::AddToolbarChoice("/Game/Art/UI/Textures/Editor/t_rotateIcon.t_rotateIcon",
                                            {"default", "center", "mirrored"}, 0);
Editor::AddHotkey(Plugins::Folder() + "keyboard_g.png", "group");

void Update(float dt)
{
    int chosen = Editor::ToolbarChoice(rotateChoice);
    ...
}

Clicks on track pieces come from Editor::NextClick: the piece clicked, the modifier keys and whether it was already selected, after the game has handled the click.

See Create Extensions for a full example.

Every window and widget function is in the UI reference.