ml::docs | Your First App Manifests Events Messaging Plugins Scenes Resources
See all tutorials →
Intermediate 4 sections ~20 min C++17/20

Panel Controls

TabbedPanel, SplitPanel, and ScrollPane — container components that own and manage their children, with automatic resizing, draggable dividers, and scrolling.

interactive preview
TabbedPanel
Notes
Settings
Log ×
Start typing your notes here…
SplitPanel
Sidebar

· Item A
· Item B
· Item C
Editor content area
ScrollPane
Row 1 — Wi-Fi
Row 2 — Bluetooth
Row 3 — Dark mode
Row 4 — Notifications
Row 5 — Privacy
Row 6 — Storage
Row 7 — Battery
Introduction

Ownership and Sizing

All three panel types take ownership of their content components. You pass a std::unique_ptr to addTab() or addPane(). If the content type has a setSize() method, the panel calls it automatically whenever the available area changes. ScrollPane uses a non-owning pointer model — you call addComponent() on it like a scene.

ClassLayoutContent ownership
ml::TabbedPanelOne panel visible at a time, selected by tabsOwns via unique_ptr
ml::SplitPanelN panes separated by draggable dividersOwns via unique_ptr
ml::ScrollPaneClipped scrollable area with a scroll barNon-owning pointer

1Panels

TabbedPanel

Displays one content panel at a time. Tabs can be placed on any edge (TOP, BOTTOM, LEFT, RIGHT). Hidden panels remain alive in memory so their state is preserved between tab switches. addTab() is a template that returns a reference to the content object, making inline configuration clean.

TabbedPanel — basic usage cpp
ml::TabbedPanel tabs;
tabs.setSize({600.f, 400.f});
tabs.setPosition({40.f, 100.f});

// addTab() takes ownership; returns a reference to the content
auto& notes    = tabs.addTab("Notes",    std::make_unique<ml::TextArea>());
auto& settings = tabs.addTab("Settings", std::make_unique<ml::List>());

// Tab with an icon texture
auto& log = tabs.addTab("Log", std::make_unique<ml::TextArea>(), &logIcon);

// Closeable tab (shows a × button)
auto& debug = tabs.addTab("Debug", std::make_unique<ml::TextArea>(),
                           nullptr, /*closeable=*/true);

addComponent(tabs);
TabbedPanel — callbacks and control cpp
tabs.onTabChanged([](std::size_t index, const std::string& label) {
    std::cout << "Active: " << label << "\n";
});

tabs.onTabClosed([](std::size_t index, const std::string& label) {
    std::cout << "Closed: " << label << "\n";
});

tabs.selectTab(1);            // show "Settings"
int active = tabs.activeTab(); // index, or -1 if empty
std::size_t count = tabs.tabCount();

// Remove a tab (content destroyed on next update frame)
tabs.removeTab(2);
TabbedPanel — query content size before adding cpp
// contentSize() returns the drawable area minus the tab strip.
// Components with setSize() are auto-resized, but you can query
// the size first if you need it during construction:
sf::Vector2f cs = tabs.contentSize();
auto myWidget = std::make_unique<MyWidget>(cs);
tabs.addTab("Widget", std::move(myWidget));

2Panels

SplitPanel

Stacks N panes separated by draggable dividers. The split can be horizontal (default) or vertical. Components with a setSize() method are resized automatically when a divider is dragged or the panel is resized. Per-pane min/max sizes prevent panes from collapsing.

SplitPanel — horizontal split cpp
ml::SplitPanel split;
split.setSize({800.f, 600.f});
split.setPosition({0.f, 30.f});

// addPane() takes ownership; second arg is the initial size along the axis
auto& sidebar = split.addPane(std::make_unique<ml::List>(), 220.f);
auto& editor  = split.addPane(std::make_unique<ml::TextArea>()); // fills remaining

// Set size constraints
split.setPaneMinSize(0, 120.f); // sidebar never smaller than 120 px
split.setPaneMinSize(1, 200.f); // editor never smaller than 200 px

split.onDividerMoved([](std::size_t divIdx, float pos) {
    std::cout << "Divider " << divIdx << " moved to " << pos << " px\n";
});

addComponent(split);
SplitPanel — vertical split with 3 panes cpp
ml::SplitPanel vsplit(ml::SplitPanelSettings::Orientation::VERTICAL);
vsplit.setSize({800.f, 600.f});
vsplit.setPosition({0.f, 0.f});

vsplit.addPane(std::make_unique<ml::Rectangle>(), 40.f);  // header
vsplit.addPane(std::make_unique<ml::TextArea>());          // content
vsplit.addPane(std::make_unique<ml::Rectangle>(), 28.f);  // status bar

vsplit.setPaneMinSize(0, 40.f);
vsplit.setPaneMinSize(2, 28.f);

addComponent(vsplit);
SplitPanel — programmatic divider control cpp
split.setDividerPosition(0, 300.f); // move divider 0 to 300 px
std::size_t n = split.paneCount();  // 2

3Panels

ScrollPane

ScrollPane clips its children into a viewport and draws a vertical scroll bar. Children are stacked vertically. Unlike TabbedPanel and SplitPanel, it holds non-owning pointers — you add children via addComponent() and register them with the scene separately if they need their own events.

ScrollPane — basic usage cpp
ml::ScrollPane pane(400.f, 300.f); // width, height
pane.setPosition({50.f, 100.f});

pane.addComponent(itemA);
pane.addComponent(itemB);
pane.addComponent(itemC);

// Styling
pane.setBackgroundColor(sf::Color(20, 20, 30));
pane.setScrollBarColor(sf::Color(100, 100, 140, 200));
pane.setScrollBarWidth(8.f);

addComponent(pane);
ScrollPane — scroll API and content height override cpp
// Programmatic scroll
float y = pane.getScrollOffsetY();
pane.setScrollOffsetY(150.f); // clamped automatically

// Override computed content height (useful when content is
// rendered externally rather than as child components)
pane.setContentHeight(2000.f);

// Resize the viewport
pane.setSize(600.f, 400.f);
ScrollPane::embed() — use as a private member cpp
// When ScrollPane is a private member of another component
// (not registered with addComponent()), call embed() to remove
// all its event subscriptions so it cannot steal focus.
// draw(), setPosition(), setSize(), and setScrollOffsetY() still work.
class MyWidget : public ml::Component<>
{
    ml::ScrollPane _pane{400.f, 300.f};
public:
    MyWidget() {
        _pane.embed(); // silence BEFORE registering any other events
    }
};