Building a Basic UI Control in SDL: Step-by-Step with UIButton

sdl-ui-control-button

In SDL game development, it’s common to need custom UI controls. This post will walk you through building a complete UIButton control from scratch, based on an existing Node/UIControl framework. The button will feature text, state changes, click events, and rendering.

I. What is a UIButton?

A UIButton is one of the most common UI elements. It can respond to mouse clicks and changes its appearance based on different states, such as:

  • Normal: The default state.
  • Hover: The state when the mouse cursor is over the button.
  • Pressed: The state when a mouse button is held down over the button.
  • Disabled: The state when the button is inactive and cannot be interacted with.

In SDL, we’ll combine the UIControl base class, event handling, and custom rendering logic to implement these features.


II. The Core Framework: Inheriting from UIControl

Our UIButton will inherit from UIControl, which itself is derived from a Node class. This provides a set of common properties:

  • Position and Size: Vec2 m_pos, m_size
  • Text, Color, Font
  • State Management: The UIState enum
  • Event Handling: Overriding the Process(SDL_Event* e) method

We’ll need to modify the base framework to add event handling. Let’s add the following declarations to the Node.h file:

C++

// Called when the mouse cursor enters the element's area
virtual void OnMouseOver() {}

// Called when the mouse cursor leaves the element's area
virtual void OnMouseOut() {}

// Handles incoming SDL events (returns true if event was consumed)
virtual bool Handle(SDL_Event* e) { return false; };

// Registers a callback function for a specific event type
void AddEvent(EventType et, std::function<void(void*)> _fn);

// Unregisters all callbacks for the specified event type
void RemoveEvent(EventType et);

// Convenience methods for adding click event handlers
void AddClick(std::function<void(void*)> _fn);
void AddRClick(std::function<void(void*)> _fn);

// Enables/disables event propagation to underlying elements
inline void SetAllowEventPassThrough(bool _allowEventPassthrouth) { m_allowEventPassthrouth = _allowEventPassthrouth; }

// Checks if events are allowed to pass through to underlying elements
inline bool IsAllowEventPassThrogh() const { return m_allowEventPassthrouth; }

protected:

// Handles mouse enter/leave detection based on current cursor position
void HandleMouseOverOut(const SDL_Point& pt);

// Gets current mouse cursor position relative to application window
SDL_Point GetMousePosition() const;

// Gets element's absolute position and size in windows coordinates
SDL_Rect GetGlobalRect() const;

// Triggers all callbacks registered for the specified event type
void FireEvent(EventType et, void* args);

// Checks if any callbacks are registered for the specified event type
bool HasEvent(EventType et);

Let’s focus on two key functions: Node::Process and Node::HandleMouseOverOut.

Node::Process is the main event loop. It first checks for SDL_MOUSEMOTION to manage hover states, and then recursively processes events for child nodes before handling its own events. Event propagation can be controlled with IsAllowEventPassThrogh.

C++

bool Node::Process(SDL_Event* e) {
    if (!m_enabled) return false;

    if (e->type == SDL_MOUSEMOTION)
    {
        // Handle mouse over and mouse out states
        SDL_Point point{ e->motion.x, e->motion.y };
        HandleMouseOverOut(point);
    }
    
    SDL_Point pt = GetMousePosition();
    bool eventHandled = false;
    SDL_Rect rc = GetGlobalRect();

    // Check if the mouse is within the control's area before processing events
    if (SDL_PointInRect(&pt, &rc))
    {
        if (!IsEnabled() || !IsVisible())
            return true;

        // Process events from top-level child controls first
        for (auto it = m_children.rbegin(); it != m_children.rend(); ++it)
        {
            if ((*it).node->Process(e))
            {
                eventHandled = true;
                // If the control is set to allow event passthrough, continue propagation. Otherwise, stop.
                if (!(*it).node->IsAllowEventPassThrogh())
                    return true;
            }
        }

        // Route the message to the actual handler function
        if (Handle(e))
        {
            eventHandled = true;
            if (!IsAllowEventPassThrogh())
                return true;
        }
    }

    return eventHandled;
}

HandleMouseOverOut detects when the mouse enters or leaves a control’s bounding box and calls the appropriate virtual function, OnMouseOver or OnMouseOut.

C++

void Node::HandleMouseOverOut(const SDL_Point& pt)
{
    if (!IsEnabled() || !IsVisible()) return;
    auto pos = GetGlobalPosition();
    SDL_Rect rc = { pos.x, pos.y, m_size.x,m_size.y };
    if (SDL_PointInRect(&pt, &rc))
    {
        if (!m_mouseover)
        {
            m_mouseover = true;
            OnMouseOver();
        }
    }
    else
    {
        if (m_mouseover)
        {
            m_mouseover = false;
            OnMouseOut();
        }
    }
    for (auto& n : m_children)
        n.node->HandleMouseOverOut(pt);
}

III. Implementing State Change Logic

Since click events are a common, fundamental event, we’ll handle them in our UIControl base class by overriding the Handle function. This function manages the UIState based on mouse events.

C++

bool UIControl::Handle(SDL_Event* e) {

    int mouseX, mouseY;
    SDL_GetMouseState(&mouseX, &mouseY);

    SDL_Rect bounds = { m_pos.x, m_pos.y, m_size.x, m_size.y };
    auto pt = SDL_Point{ mouseX, mouseY };
    bool isHovering = SDL_PointInRect(&pt, &bounds);

    switch (e->type) {
    case SDL_MOUSEMOTION:
        if (isHovering && m_state != UIState::Hovered) {
            m_state = UIState::Hovered;
        }
        else if (!isHovering && m_state == UIState::Hovered) {
            m_state = UIState::Normal;
        }
        break;

    case SDL_MOUSEBUTTONDOWN:
        if (isHovering && e->button.button == SDL_BUTTON_LEFT) {
            m_state = UIState::Pressed;
        }
        break;

    case SDL_MOUSEBUTTONUP:
        if (m_state == UIState::Pressed && isHovering && e->button.button == SDL_BUTTON_LEFT) {
            m_state = UIState::Hovered;
            FireEvent(EventType::ET_LCLICK, this);
        }
        else {
            m_state = isHovering ? UIState::Hovered : UIState::Normal;
        }
        break;

    default:
        break;
    }

    return false;
}

IV. Rendering the Button Style

We’ll render the background color and text based on the current m_state. Our control will have an array m_default_background_color and an array of textures SDL_Texture* m_tex[UIState::UIStateLength]{}. The system will prioritize drawing with the texture if it’s available; otherwise, it will fall back to using the default color for that state. The button’s text is then rendered on top.

C++

void UIButton::Render()
{
	SDL_Rect rc = GetGlobalRect();
	if (m_tex[0])
		sRender->DrawImage(m_tex[m_state], rc.x, rc.y);
	else
		sRender->FillRect( rc, m_default_backgroud_color[m_state]);
	if (!m_text.empty())
		sRender->DrawString2(m_text.c_str(), 14, rc, SDL_Color{ 0,0,0,255 });
}

You can also set the button’s text properties using the same methods as our previous Label control.

The control also provides a void SetResource(SDL_Texture** _tex) method for setting custom textures for more visually appealing UI.


V. Binding Click Events

We provide an AddEvent interface to bind click callbacks, which you can use like this:

C++

button->AddEvent(EventType::ET_LCLICK,[]() {
    SDL_Log("Button clicked!");
});

For convenience, you can also use the AddLClick shortcut function:

C++

button->AddLClick([]() {
    SDL_Log("Button clicked!");
});

Using std::function provides a flexible callback mechanism that can be adapted for different use cases.


VI. Summary and Future Directions

With these steps, we’ve completed a fully functional UIButton that supports state changes, click events, and custom rendering.

If you are developing a UI system with SDL, this structure can help you quickly build a flexible and extensible set of controls.

You can find the accompanying code for this post here: https://github.com/84378996/simple_sdl_gui_tutorials/tree/main/tutorials03

Here’s the final result in action:

Leave a Reply