Step-by-Step Guide to Basic SDL UI Controls: UIListBox

sdl-ui-contro-listbox

After implementing controls like UIButton and UITextField, this article will focus on a very common UI element: the ListBox. It’s widely used in interface development for scenarios like file selection, menu options, and displaying data lists.

The core features of UIListBox include:

  • Dynamically adding and removing list items.
  • Mouse click selection with item highlighting.
  • Mouse wheel scrolling to view content that exceeds the display area.
  • Event callbacks for seamless interaction with external logic.

Let’s dive into the implementation and usage of this control step by step.


I. Data Structure Definition

First, we need a ListItem struct to hold the data for each row:

C++

struct ListItem
{
    std::string m_text;      // The text to display
    SDL_Texture* m_icon;     // Optional icon (not used in this example)
    bool m_checked;          // Checked state (for future expansion)
    void* data;              // User-defined data
};

The UIListBox internally maintains a std::vector<ListItem> as its actual data source.


II. Basic Class Framework

UIListBox inherits from UIScrollView, so it naturally has scrollbar support. The key members of the class are as follows:

C++

class UIListBox : public UIScrollView
{
public:
    UIListBox();
    ~UIListBox();

    void AddItem(const ListItem& item);
    void AddItem(const std::string& item);
    void RemoveAt(size_t _index);
    void RemoveAll();
    int GetItemCount() const;
    inline void SetSelectedIndex(int index) { m_selectedIndex = index; }
    inline int GetSelectedIndex() const { return m_selectedIndex; }
    const ListItem* GetSelectedItem() const;

    void SetItemData(size_t ix, void* data);
    void* GetItemData(size_t ix);
    void SetItemText(size_t ix, const std::string& _name);
    void SetItemSelected(int ix);

    void Render() override;
    bool Handle(SDL_Event* e) override;
    void OnMove(int x, int y) override;

protected:
    std::vector<ListItem> m_items;  // List of items
    int m_selectedIndex{ -1 };      // Index of the currently selected item
    SDL_Point m_ptHit;
    SDL_Point m_ptThum;
    int m_offsetHit{ 0 };
};

Key Points:

  • m_items stores all the list entries.
  • m_selectedIndex tracks the index of the currently selected item.
  • The scrolling functionality is achieved by combining SetLineCount and m_scrollOffset with the UIScrollView base class.

III. Adding and Removing Items

There are two ways to add items: by passing a ListItem struct or just a string.

Adding Items:

C++

void UIListBox::AddItem(const ListItem& item)
{
    m_items.push_back(item);
    SetLineCount(m_items.size());
}

void UIListBox::AddItem(const std::string& item)
{
    m_items.push_back({ item, nullptr, false, 0 });
    SetLineCount(m_items.size());
}

Removing and Clearing Items:

C++

void UIListBox::RemoveAt(size_t _index)
{
    m_items.erase(m_items.begin() + _index);
    SetLineCount(m_items.size());
    if (m_selectedIndex >= _index)
    {
        m_selectedIndex--;
    }
}

void UIListBox::RemoveAll()
{
    m_items.clear();
    SetLineCount(m_items.size());
}

IV. Rendering Logic

The rendering process has three main steps:

  1. Drawing the background and border.
  2. Calculating the range of visible items based on the scroll position.
  3. Drawing the items, with the selected item highlighted.

C++

void UIListBox::Render()
{
    auto rcClient = GetGlobalRect();
    sRender->FillRect(rcClient, SDL_Color{ 255,255,255,255 });
    sRender->DrawRect(rcClient, SDL_Color{ 50, 157, 77, 255 });
    __super::Render();

    SDL_Rect _rc = rcClient;
    SDL_RenderSetClipRect(sRender->GetRender(), &_rc);

    int startItem = m_scrollOffset / (m_lineheight+m_linespace);
    int endItem = SDL_min(startItem + m_size.y / (m_lineheight + m_linespace), GetItemCount());

    for (int i = startItem; i < endItem; ++i) {
        SDL_Rect itemRect = { rcClient.x, rcClient.y + (i - startItem) * (m_lineheight + m_linespace), m_size.x - _BAR_SIZE, (m_lineheight + m_linespace) };
        if (m_selectedIndex == static_cast<int>(i)) {
            sRender->FillRect(itemRect, SDL_Color{ 200, 200, 200, 255 });
        }
        SDL_Color textColor = (m_selectedIndex == static_cast<int>(i)) ? SDL_Color{ 255, 255, 255, 255 } : SDL_Color{ 0, 0, 0, 255 };

        auto [textW, textH] = sRender->GetTextSize(m_items[i].m_text.c_str(), m_fontSize);
        sRender->DrawString(m_items[i].m_text.c_str(), m_fontSize, SDL_Point{ itemRect.x + 5, itemRect.y + (itemRect.h - textH) / 2 });
    }

    auto rcWnd = sRender->GetClientRect();
    SDL_RenderSetClipRect(sRender->GetRender(), &rcWnd);
}

V. Event Handling

Event handling is divided into two types:

  • Mouse Clicks: Determine the item corresponding to the click position and update m_selectedIndex.
  • Mouse Wheel: Scroll the list up or down.

C++

bool UIListBox::Handle(SDL_Event* e)
{
    if (e->type == SDL_MOUSEBUTTONDOWN)
    {
        SDL_Point pt{ e->button.x,e->button.y };
        auto rcClient = GetGlobalRect();
        SDL_Rect rc{ rcClient.x,rcClient.y, m_size.x - _BAR_SIZE, m_size.y };
        if (SDL_PointInRect(&pt,&rc))
        {
            int clickedItem = (int)(pt.y + m_scrollOffset - rcClient.y) / (m_lineheight + m_linespace);
            if (clickedItem >= 0 && clickedItem < GetItemCount())
            {
                FireEvent(EventType::ET_ITEM_CLICK, &clickedItem);
                if (m_selectedIndex != clickedItem)
                {
                    m_selectedIndex = clickedItem;
                    FireEvent(EventType::ET_ITEMSELECTIONCHANGED, &m_selectedIndex);
                }
            }
            return true;
        }
    }
    else if (e->type == SDL_MOUSEWHEEL)
    {
        if (e->wheel.y > 0) OnScrollUp();
        if (e->wheel.y < 0) OnScrollDown();
        return true;
    }
    return __super::Handle(e);
}

Using FireEvent, external logic can easily receive click and selection change events.


VI. Usage Example

Here’s a complete usage scenario:

C++

auto listbox = std::shared_ptr<UIListBox>(new UIListBox);
listbox->SetPosition({ 340, 20 });
listbox->SetSize({ 120,150 });
listbox->AddEvent(EventType::ET_ITEMSELECTIONCHANGED, [=](void*) 
{
    int ix = listbox->GetSelectedIndex();
    char txt[64];
    sprintf_s(txt, "listbox item at index %d selected", ix);
    m_lbmsg->SetText(txt);
});
AddNode(listbox);

// Add button
auto btn = std::shared_ptr<UIButton>(new UIButton);
btn->SetText("AddItem");
btn->SetPosition({ 340,175 });

static int itemindex = 0;
btn->AddClick([=](void*)
{
    char itemtext[64];
    sprintf_s(itemtext, "item %d", ++itemindex);
    listbox->AddItem(itemtext);
});
AddNode(btn);

// Delete button
btn = std::shared_ptr<UIButton>(new UIButton);
btn->SetText("RemoveSelectedItem");
btn->SetPosition({ 340,205 });
btn->SetSize({ 150,25 });
btn->AddClick([=](void*)
{
    int sel = listbox->GetSelectedIndex();
    if (sel >= 0)
        listbox->RemoveAt(sel);
});
AddNode(btn);

Final Result:

  • Clicking “AddItem” dynamically adds new items.
  • Selecting an item triggers an event, updating the prompt text.
  • Clicking “RemoveSelectedItem” deletes the currently selected item.

VII. Summary

With UIListBox, we’ve created a fully functional list control that supports:

  • Adding, removing, and modifying items.
  • Mouse clicks and mouse wheel scrolling.
  • Selection state and event notifications.

Together with the previously implemented UIButton and UITextField controls, we are gradually building a small SDL UI framework.


Future Enhancements:

The following features are not yet implemented but could be added:

  • Displaying icons (ListItem::m_icon).
  • Check Box mode (ListItem::m_checked).
  • Keyboard navigation (up/down arrow keys for selection, Enter for confirmation).

If you need these features, feel free to implement them yourself or leave a comment below.

Source Code for this Chapter: https://github.com/84378996/simple_sdl_gui_tutorials/tree/main/tutorials09

Demo Video:

Leave a Reply