Interactive Buttons
The Button widget provides a clickable UI element with built-in state management, visual feedback, and gamepad/keyboard navigation. Internally, a Button contains a Quad (background) and a Text (label) as children.
Creating a Button
C++
#include "Nodes/Widgets/Button.h"
Button* btn = parentWidget->CreateChild<Button>("PlayButton");
btn->SetTextString("Play");
btn->SetDimensions(200.0f, 50.0f);
btn->SetAnchorMode(AnchorMode::Mid);
Lua
local btn = self:CreateChild("Button")
btn:SetName("PlayButton")
btn:SetTextString("Play")
btn:SetDimensions(200.0, 50.0)
btn:SetAnchorMode(AnchorMode.Mid)
Editor: Add a Button node in the scene hierarchy. Set the text string, textures, and colors in the Properties panel.
Handling Button Clicks
Lua — Script Function
The simplest approach: define an OnActivated() function on a script attached to the Button node. The engine calls it automatically when the button is activated.
function MyButton:OnActivated()
Log.Debug("Button was clicked!")
end
Lua — Signal Connection
Connect to the "Activated" signal to handle clicks on buttons that are children of your script node:
function MyMenu:Start()
self.playBtn = self:CreateChild("Button")
self.playBtn:SetTextString("Play")
self.playBtn:SetDimensions(200.0, 50.0)
self.playBtn:SetAnchorMode(AnchorMode.Mid)
self.playBtn:ConnectSignal("Activated", self, MyMenu.OnPlayClicked)
end
function MyMenu:OnPlayClicked()
Log.Debug("Play button clicked!")
end
C++ — Signal Connection
Use ConnectSignal with a SignalHandlerFP function pointer:
#include "Nodes/Widgets/Button.h"
static void HandlePlayClicked(Node* listener, const std::vector<Datum>& args)
{
// args[0] contains the Button* that was activated
LogDebug("Play button clicked!");
}
void MyMenu::Create()
{
Canvas::Create();
mPlayBtn = CreateChild<Button>("Play");
mPlayBtn->SetTextString("Play");
mPlayBtn->SetDimensions(200.0f, 50.0f);
mPlayBtn->SetAnchorMode(AnchorMode::Mid);
mPlayBtn->ConnectSignal("Activated", this, HandlePlayClicked);
}
C++ — Virtual Override
If you subclass Button, override Activate():
class PlayButton : public Button
{
DECLARE_NODE(PlayButton, Button);
virtual void Activate() override
{
Button::Activate(); // emits signals
// Custom logic here
}
};
Button States
Buttons have four visual states:
| State | When |
|---|---|
Normal |
Default idle state |
Hovered |
Mouse cursor is over the button |
Pressed |
Mouse button is held down on the button |
Locked |
Button is disabled, ignores all input |
Check or set the current state:
local state = btn:GetState()
if state == ButtonState.Locked then
-- button is locked
end
Per-State Textures
Assign different textures for each state to give visual feedback:
C++
btn->SetNormalTexture(LoadAsset<Texture>("T_BtnNormal"));
btn->SetHoveredTexture(LoadAsset<Texture>("T_BtnHovered"));
btn->SetPressedTexture(LoadAsset<Texture>("T_BtnPressed"));
btn->SetLockedTexture(LoadAsset<Texture>("T_BtnLocked"));
Lua
btn:SetStateTextures(
LoadAsset("T_BtnNormal"),
LoadAsset("T_BtnHovered"),
LoadAsset("T_BtnPressed"),
LoadAsset("T_BtnLocked")
)
Pass nil for any state you don't need a unique texture for.
Per-State Colors
Set different colors for each state (applied to the internal Quad by default):
C++
btn->SetNormalColor({0.5f, 0.5f, 0.5f, 1.0f});
btn->SetHoveredColor({0.7f, 0.7f, 0.7f, 1.0f});
btn->SetPressedColor({0.3f, 0.3f, 0.3f, 1.0f});
btn->SetLockedColor({0.2f, 0.2f, 0.2f, 0.5f});
Lua
btn:SetStateColors(
Vec(0.5, 0.5, 0.5, 1.0), -- normal
Vec(0.7, 0.7, 0.7, 1.0), -- hovered
Vec(0.3, 0.3, 0.3, 1.0), -- pressed
Vec(0.2, 0.2, 0.2, 0.5) -- locked
)
Control whether state colors apply to the Quad, the Text, or both:
// C++
btn->SetUseQuadStateColor(true); // apply state colors to background (default: true)
btn->SetUseTextStateColor(false); // apply state colors to text (default: false)
Locking / Unlocking
Lock a button to disable all interaction:
btn:SetLocked() -- sets state to Locked
To unlock, you would need to set the state back by interacting with it or managing state manually.
Accessing Internal Widgets
A Button's internal Quad and Text are accessible for fine-grained control:
local quad = btn:GetQuad()
local text = btn:GetText()
-- Customize the text widget directly
text:SetTextSize(20.0)
text:SetFont(LoadAsset("F_CustomFont"))
-- Customize the quad directly
quad:SetTexture(LoadAsset("T_CustomBackground"))
Gamepad / Keyboard Navigation
Connect buttons into a navigation graph so players can move between them with a gamepad D-pad or arrow keys:
Lua
function MyMenu:Start()
self.playBtn = self:CreateChild("Button")
self.playBtn:SetTextString("Play")
self.playBtn:SetAnchorMode(AnchorMode.Mid)
self.playBtn:SetPosition(0.0, -30.0)
self.playBtn:SetDimensions(200.0, 40.0)
self.optionsBtn = self:CreateChild("Button")
self.optionsBtn:SetTextString("Options")
self.optionsBtn:SetAnchorMode(AnchorMode.Mid)
self.optionsBtn:SetPosition(0.0, 20.0)
self.optionsBtn:SetDimensions(200.0, 40.0)
self.quitBtn = self:CreateChild("Button")
self.quitBtn:SetTextString("Quit")
self.quitBtn:SetAnchorMode(AnchorMode.Mid)
self.quitBtn:SetPosition(0.0, 70.0)
self.quitBtn:SetDimensions(200.0, 40.0)
-- Set up vertical navigation
self.playBtn:SetNavigation(nil, self.optionsBtn, nil, nil) -- down -> options
self.optionsBtn:SetNavigation(self.playBtn, self.quitBtn, nil, nil) -- up -> play, down -> quit
self.quitBtn:SetNavigation(self.optionsBtn, nil, nil, nil) -- up -> options
-- Select first button for gamepad/keyboard users
Button.SetSelected(self.playBtn)
end
C++
mPlayBtn->SetNavUp(nullptr);
mPlayBtn->SetNavDown(mOptionsBtn);
mOptionsBtn->SetNavUp(mPlayBtn);
mOptionsBtn->SetNavDown(mQuitBtn);
mQuitBtn->SetNavUp(mOptionsBtn);
mQuitBtn->SetNavDown(nullptr);
Button::SetSelectedButton(mPlayBtn);
Navigation parameters for SetNavigation() are: (up, down, left, right). Pass nil/nullptr for directions that have no target.
Enabling / Disabling Input Types
Control which input methods buttons respond to globally:
Button.EnableMouseHandling(true)
Button.EnableGamepadHandling(true)
Button.EnableKeyboardHandling(true)
// C++
Button::SetHandleMouse(true);
Button::SetHandleGamepad(true);
Button::SetHandleKeyboard(true);
Disable mouse handling when you want gamepad-only UI, or vice versa.
State Change Signal
Connect to the "StateChanged" signal to react when a button's visual state changes (e.g., for sound effects):
btn:ConnectSignal("StateChanged", self, MyMenu.OnButtonStateChanged)
function MyMenu:OnButtonStateChanged()
-- Play hover sound, update visuals, etc.
end
Right-Click Support
Enable right-click activation:
// C++
btn->EnableRightClickPress(true);
// Lua (via C++ only — not exposed to Lua)
Example: Menu with Multiple Buttons
function MainMenu:Start()
self:SetAnchorMode(AnchorMode.FullStretch)
self:SetRatios(0.0, 0.0, 1.0, 1.0)
-- Background
local bg = self:CreateChild("Quad")
bg:SetAnchorMode(AnchorMode.FullStretch)
bg:SetRatios(0.0, 0.0, 1.0, 1.0)
bg:SetTexture(LoadAsset("T_MenuBg"))
-- Title
local title = self:CreateChild("Text")
title:SetAnchorMode(AnchorMode.TopMid)
title:SetPosition(0.0, 40.0)
title:SetDimensions(400.0, 60.0)
title:SetTextSize(48.0)
title:SetText("My Game")
title:SetHorizontalJustification(Justification.Center)
title:SetColor(Vec(1, 1, 1, 1))
-- Buttons
local buttonNames = {"Play", "Options", "Quit"}
self.buttons = {}
for i, name in ipairs(buttonNames) do
local btn = self:CreateChild("Button")
btn:SetTextString(name)
btn:SetAnchorMode(AnchorMode.Mid)
btn:SetPosition(0.0, (i - 1) * 60.0 - 30.0)
btn:SetDimensions(220.0, 45.0)
btn:ConnectSignal("Activated", self, MainMenu.OnButtonClicked)
self.buttons[i] = btn
end
-- Navigation
for i, btn in ipairs(self.buttons) do
local up = self.buttons[i - 1]
local down = self.buttons[i + 1]
btn:SetNavigation(up, down, nil, nil)
end
Button.SetSelected(self.buttons[1])
end
function MainMenu:OnButtonClicked()
local selected = Button.GetSelected()
if selected == self.buttons[1] then
-- Play
elseif selected == self.buttons[2] then
-- Options
elseif selected == self.buttons[3] then
-- Quit
end
end