Lysa UI  0.0
Lysa UI —UI components for the Lysa Engine
How To Use

Table of contents


1. Setting up the WindowManager

Create a WindowManager in your application entry point, passing only the RenderingWindow it draws into. The manager subscribes to the engine process and input events automatically, creates the default style, and uses a font embedded in the library:

int lysaMain() {
lysa::ContextConfiguration contextConfiguration { /* ... */ };
lysa::Lysa lysa(contextConfiguration);
MainWindow window;
// Create the UI manager : embedded default font, default style, engine events
lysa::ui::WindowManager windowManager(window);
// Instantiate and attach a scene that also uses the UI
auto scene = std::make_unique<nodes::GameScene>(windowManager);
scene->attach(window);
lysa::ctx().events.subscribe(lysa::MainLoopEvent::PROCESS,
[&](const lysa::Event&) {
window.getRenderTarget().render();
});
lysa.run();
return 0;
}

The defaults are read by every Window that does not set its own, and changing one refreshes all of them:

// Scale applied to the default font
windowManager.setDefaultFontScale(0.25f);
// Replace the embedded font
auto font = std::make_shared<lysa::Font>("app://res/fonts/Signwood");
windowManager.setDefaultFont(font);
// The default color of the texts belongs to the style, not to the windows
windowManager.getDefaultStyle().setOption("color_text", "1.0,1.0,1.0,1.0");

2. Creating a UI Window

A Window is a virtual overlay panel managed by WindowManager. Create one with a rectangle and configure its style and root widget before adding child widgets:

// Full-screen overlay window
const auto uiWindow = windowManager.create(lysa::RECT_FULLSCREEN);
// OR a fixed-size positioned window
const auto dialog = windowManager.create(lysa::Rect{100.0f, 100.0f, 400.0f, 300.0f});
// Allow the user to resize from the right and bottom edges
dialog->setResizeableBorders(
dialog->setMinimumSize(200.0f, 150.0f);

Subclass Window to respond to lifecycle events:

class HUDWindow : public lysa::ui::Window {
public:
HUDWindow() : Window(lysa::RECT_FULLSCREEN) {}
void onCreate() override {
// build the widget tree here
}
void onResize() override {
// re-layout widgets when the window is resized
}
};
const auto hud = windowManager.add(std::make_shared<HUDWindow>());

3. Creating and adding widgets

Use the create<T>() template on a Window or Widget to construct and add a child in one step. The first argument is a resource string (size or style hint), the second is the Alignment:

// Create a centered panel of size 200x300
const auto panel = uiWindow->create<lysa::ui::Box>(
"width=200;height=300", lysa::ui::Alignment::CENTER);
// Add a button inside the panel
const auto button = panel->create<lysa::ui::Button>(
"width=100;height=40", lysa::ui::Alignment::TOPCENTER);
// Add a text label inside the button

Widgets can also be constructed separately and added with add<T>():

auto label = std::make_shared<lysa::ui::Text>("Score: 0");
label->setTextColor(lysa::float4{1.0f, 1.0f, 0.0f, 1.0f});

Remove a specific child or clear the entire tree:

panel->remove(button);
panel->removeAll();

4. Alignment and layout

The Alignment enum controls how each child is stacked inside its parent's content area. Children are laid out in the order they are added.

// Fill the entire parent
// Stack buttons along the left edge : the label of a button is a Text child
for (const auto& label : {"OK", "Cancel"}) {
const auto button = panel->create<lysa::ui::Button>(
"width=80;height=30", lysa::ui::Alignment::LEFT);
}
// Pin a label to the bottom-right corner

Use padding to add space between stacked children:

panel->setPadding(8.0f);

Use borders to add internal margins inside a widget:

panel->setHBorder(10.0f);
panel->setVBorder(6.0f);

5. Handling events

All widget signals are dispatched through the engine's event system. Subscribe using the widget's id and a UIEvent constant. Events are queued and executed on the next frame and the parameters of the event are carried in a lysa::Event::payload and read with std::any_cast :

// Button click : OnClick has no parameter
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnClick, button->id,
[](const lysa::Event&) {
lysa::Log::info("Button clicked!");
});
// React when a TextEdit changes
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnTextChange, editField->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<const lysa::ui::UIEventText&>(e.payload);
lysa::Log::info("New text: " + payload.text);
});
// Value changed in a scroll bar
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnValueChange, scrollBar->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<const lysa::ui::UIEventValue&>(e.payload);
lysa::Log::info("Value: " + std::to_string(payload.value));
});

subscribe() returns the identifier of the handler. Keep it in a variable to remove the handler with lysa::ctx().events.unsubscribe(handlerId) before the widget is destroyed.


6. Text widget

Text displays a single immutable line of text. Wrap it inside a Box or Button for a bordered appearance:

// Simple label
const auto label = uiWindow->create<lysa::ui::Text>(
label->setTextColor(lysa::float4{0.0f, 1.0f, 0.0f, 1.0f});
label->setFontScale(1.5f);
// Update at runtime
label->setText("Health: 75");

Query the natural size of a Text widget before sizing its container:

float w, h;
label->getSize(w, h);
container->setSize(w + 20.0f, h + 10.0f);

7. TextEdit widget

TextEdit provides a single-line editable text field with cursor and selection support:

const auto edit = panel->create<lysa::ui::TextEdit>(
"width=200;height=28", lysa::ui::Alignment::CENTER, "Enter name");
// Read-only mode
edit->setReadOnly(true);
// Programmatic text update
edit->setText("Player One");
// Move the cursor
edit->setSelStart(0);
// Listen for changes
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnTextInput, edit->id,
[edit](const lysa::Event&) {
lysa::Log::info("Input: " + edit->getText());
});

8. RichTextArea widget

RichTextArea displays a read-only block of word-wrapped text marked up with BBCode:

const auto area = panel->create<lysa::ui::RichTextArea>(
"width=400;height=200",
"[center][b]Release notes[/b][/center][br]"
"The [i]Lysa[/i] engine now supports [color=orange]rich text[/color].[br]"
"Read the [url=https://lysaengine.org]documentation[/url] or run [code]lysa --help[/code].");

Supported tags :

Tag Effect
[b] [i] Bold and italic; drawn with the faces given to setFonts() (bold is emulated when no bold face is given)
[u] [s] Underlined and struck-through text
[code] Monospaced text drawn with the code color
[color=…] Color of the text, as a name (red, orange, silver, …), #rgb / #rrggbb / #rrggbbaa, or r,g,b / r,g,b,a floats
[size=n] Size of the text relative to RichTextArea::SIZE_REFERENCE (100 = the size of the widget), clamped to [MIN_SIZE_FACTOR, MAX_SIZE_FACTOR]
[url=target] Clickable link; the payload of OnClick carries target or the link text when the tag has no value
[left] [center] [right] Horizontal placement of the line
[br] Line break

An unknown or unterminated tag is displayed as-is.

Colors, fonts and scrolling:

// Colors : the text color defaults to the color of the style (`color_text` option)
area->setTextColor({0.9f, 0.9f, 0.9f, 1.0f});
area->setLinkColor({0.4f, 0.7f, 1.0f, 1.0f});
area->setCodeColor({0.8f, 0.8f, 0.5f, 1.0f});
// Faces used by [b], [i] and [code]
area->setFonts(boldFont, italicFont, boldItalicFont, monoFont);
// Applied to every color read from the markup, e.g. to darken a light theme
area->setColorConverter(
[](const lysa::float4& color) { return lysa::float4{color.rgb * 0.8f, color.a}; });
area->scrollToTop();
area->scrollToBottom();

Clicking a link:

lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnClick, area->id,
[](const lysa::Event& event) {
const auto& link = std::any_cast<const lysa::ui::UIEventLink&>(event.payload);
lysa::Log::info("Link clicked: " + link.target);
});

9. Image widget

Image displays a lysa::Image resource. By default it auto-sizes to the image dimensions:

// Auto-sized image, loaded from its URI by the widget itself
const auto imgWidget = panel->create<lysa::ui::Image>(lysa::ui::Alignment::CENTER, true);
imgWidget->setImage("app://res/ui/logo.png");
// Fixed-size image with a color tint
const auto icon = panel->create<lysa::ui::Image>(
"width=32;height=32", lysa::ui::Alignment::LEFT, false);
icon->setImage("app://res/ui/icon.png");
icon->setColor(lysa::float4{1.0f, 0.5f, 0.5f, 1.0f});
// An already loaded lysa::Image can be given to the constructor or set later
auto& logo = lysa::ctx().res.get<lysa::ImageManager>().load("app://res/ui/logo.png");
const auto other = panel->create<lysa::ui::Image>(lysa::ui::Alignment::CENTER, logo, true);
icon->setImage(logo);

10. ScrollBar widget

ScrollBar (and its aliases HScrollBar / VScrollBar) lets the user select a value within a numeric range:

// Horizontal bar: range 0–100, initial value 50, step 1
const auto hBar = panel->create<lysa::ui::HScrollBar>(
"width=200;height=20", lysa::ui::Alignment::BOTTOM,
0.0f, 100.0f, 50.0f, 1.0f);
// Vertical bar
const auto vBar = panel->create<lysa::ui::VScrollBar>(
"width=20;height=150", lysa::ui::Alignment::RIGHT,
0.0f, 200.0f, 0.0f, 5.0f);
// Change range and value programmatically
hBar->setMin(0.0f);
hBar->setMax(255.0f);
hBar->setValue(128.0f);
// Listen for changes
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnValueChange, hBar->id,
[hBar](const lysa::Event&) {
applyBrightness(hBar->getValue());
});

The two buttons scrolling by one step at the end of the bar are a SpinButtons widget, with the auto-repeat when a button is held down :

// TOP or BOTTOM for a vertical bar, LEFT or RIGHT for an horizontal one
hBar->setButtonsPlacement(lysa::ui::ScrollBar::LEFT);
vBar->getButtons()->setRepeatInterval(0); // one step per click, no repeat
vBar->getButtons()->setVisible(false); // a bar without buttons

11. ToggleButton widget

ToggleButton is a two-state button (checked / unchecked). Query or change the state programmatically, or listen for UIEvent::OnStateChange:

const auto toggle = panel->create<lysa::ui::ToggleButton>(
"width=120;height=30", lysa::ui::Alignment::CENTER);
// Set initial state
// React to toggles
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnStateChange, toggle->id,
[toggle](const lysa::Event&) {
const bool on = toggle->getState() == lysa::ui::CheckWidget::CHECK;
lysa::Log::info(std::string("Fullscreen: ") + (on ? "ON" : "OFF"));
});

12. TreeView widget

TreeView displays a hierarchical list of widgets with expand/collapse handles, a vertical scroll bar, a selection highlight and a highlight of the item below the mouse cursor :

const auto tree = panel->create<lysa::ui::TreeView>(
"width=250;height=300", lysa::ui::Alignment::LEFT);
// Add root items : addItem() returns the TreeView::Item wrapping the widget
const auto& rootA = tree->addItem(
std::make_shared<lysa::ui::Text>("Root A"));
const auto& rootB = tree->addItem(
std::make_shared<lysa::ui::Text>("Root B"));
// Add child items
const auto& childA1 = tree->addItem(rootA,
std::make_shared<lysa::ui::Text>("Child A.1"));
tree->addItem(rootA,
std::make_shared<lysa::ui::Text>("Child A.2"));

The resources of the background box, scroll bar, level indentation and the two highlights are assigned by the style (call setResources() to override them).

Expand and collapse items. expand() emits UIEvent::OnExpandItem, expandAll() does not :

tree->expand(rootA); // expand one item
tree->expand(childA1, false); // collapse it
tree->expand(someWidget); // expand the item displaying a given widget
tree->expandAll(); // expand every item at once
tree->expandAll(false); // collapse every item at once

Selection is single by default; SelectionMode::MULTI lets the user extend the selection with SHIFT (range) and CONTROL (individual items), SelectionMode::NONE disables it :

tree->setSelectionMode(lysa::ui::TreeView::SelectionMode::MULTI);
tree->select(childA1); // select, emits OnSelectItem
tree->select(childA1, false); // unselect
tree->unselectAll();
const auto item = tree->getSelectedItem(); // first selected in display order, or nullptr
const auto items = tree->getSelectedItems(); // every selected item, in display order
const auto hover = tree->getPointedItem(); // item below the mouse cursor, or nullptr
// Retrieve the item displaying a given widget
const auto found = tree->getItem(someWidget);

Reacting to the user interaction with : UIEventItem::item :

lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnSelectItem, tree->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventItem>(e.payload);
if (payload.item != nullptr) { openInInspector(payload.item); }
});
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnDoubleClickItem, tree->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventItem>(e.payload);
activate(payload.item);
});
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnExpandItem, tree->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventExpand>(e.payload);
lysa::Log::info(payload.expanded ? "expanded" : "collapsed");
});
// Clear the entire tree
tree->removeAllItems();

13. ListBox widget

ListBox displays a scrollable list of arbitrary widgets with keyboard navigation (Up/Down arrows) and a Selection highlight rectangle behind the selected item:

const auto list = panel->create<lysa::ui::ListBox>(
"width=200;height=180", lysa::ui::Alignment::LEFT);
// Add items : each item is an arbitrary Widget
for (int i = 0; i < 10; ++i) {
auto label = std::make_shared<lysa::ui::Text>("Item " + std::to_string(i));
list->addItem(label, lysa::ui::Alignment::LEFT, "width=20;height=20");
}
// Programmatic selection
list->select(0);
// Query selection
int idx = list->getSelectedIndex(); // -1 when nothing selected
auto item = list->getSelectedItem(); // nullptr when nothing selected
// Remove one item or clear all
list->removeItem(2);
list->removeAllItems();
// React to selection changes
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnSelectItem, list->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<const lysa::ui::UIEventItem&>(e.payload);
lysa::Log::info("Selected index: " + std::to_string(payload.index));
});

The inner box, the scroll bar and the selection highlight are styled by the active style; call setResources(resBox, resScroll, resSel) only to override them.


14. ProgressBar widget

ProgressBar (and its aliases HProgressBar / VProgressBar) fills proportionally to its current value within a [min, max] range. An optional text overlay can display the percentage or the raw value:

// Horizontal bar: range 0–100, initial value 30
const auto bar = panel->create<lysa::ui::HProgressBar>(
"width=200;height=20", lysa::ui::Alignment::TOP,
0.0f, 100.0f, 30.0f);
// Show percentage text over the bar
// Update value at runtime
bar->setValue(75.0f);
// Vertical variant
const auto vBar = panel->create<lysa::ui::VProgressBar>(
"width=20;height=150", lysa::ui::Alignment::RIGHT,
0.0f, 100.0f, 50.0f);

15. ScrollBox widget

ScrollBox is a bordered container that automatically adds horizontal and vertical scroll bars when its content overflows. Add children via addContent() rather than create<T>():

const auto sbox = panel->create<lysa::ui::ScrollBox>(
"width=300;height=200", lysa::ui::Alignment::CENTER);
// Add content widgets : addContent() returns the added widget
std::shared_ptr<lysa::ui::Widget> firstRow;
for (int i = 0; i < 20; ++i) {
const auto row = sbox->addContent(
std::make_shared<lysa::ui::Text>("Row " + std::to_string(i)),
"width=300;height=20");
if (i == 0) { firstRow = row; }
}
// Access the inner box directly if needed
const auto inner = sbox->getInnerBox();
// Remove individual content or clear all
sbox->removeContent(firstRow);
sbox->removeAllContent();

The inner box and the two scroll bars are styled by the active style; call setResources(resBox, resVScroll, resHScroll) only to override them.


16. Popup widget

Popup is a Panel placed at fixed (x, y) coordinates relative to its parent and always drawn on top of all sibling widgets. Use it for context menus, tooltips, or floating overlays:

// Create a popup at position (50, 80) inside uiWindow
const auto pop = uiWindow->create<lysa::ui::Popup>(
"width=120;height=90", lysa::ui::Alignment::NONE, 50.0f, 80.0f);
// Show or hide it on demand
pop->setVisible(true);
pop->setVisible(false);

Tooltip is a Popup displaying a short help text in a Box :

// Hang a tooltip under the mouse cursor
const auto tip = uiWindow->add(
std::make_shared<lysa::ui::Tooltip>("Rotate the camera\nHold the right button", x, y),
// Change the text, the tooltip resizes itself
tip->setText("Zoom in & out");
// Remove it when the mouse leaves the widget
uiWindow->remove(tip);

17. CheckBox and RadioButton widgets

Both derive from CheckWidget.

CheckBox displays a CheckMark inside its box when checked :

shadows->setState(lysa::ui::CheckWidget::CHECK);
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnStateChange, shadows->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventState>(e.payload);
enableShadows(payload.state == lysa::ui::CheckWidget::CHECK);
});

RadioButton is exclusive : checking one unchecks every sibling radio button of the same parent sharing its group index (zero by default).

const auto perspective = row->create<lysa::ui::RadioButton>(lysa::ui::Alignment::LEFTCENTER);
perspective->setState(lysa::ui::CheckWidget::CHECK); // unchecks orthogonal
// A second, independent group inside the same parent
low->setGroupIndex(1);
high->setGroupIndex(1);

18. CheckMark, CrossMark and Arrow widgets

These three widgets draw a symbol inside their rectangle, with a transparent background.

// Marks : sized like a check box by the style unless the resource string says otherwise
const auto ko = row->create<lysa::ui::CrossMark>("width=12;height=12", lysa::ui::Alignment::LEFTCENTER);
ok->setVisible(false); // show it only when the row is validated
// Arrow : pass the direction to the constructor, change it at runtime
const auto handle = row->create<lysa::ui::Arrow>(

TreeView uses an Arrow for its expand/collapse handles and CheckBox uses a CheckMark, both created internally.


19. Menu widget

Menu stacks text items vertically, each with an optional right-aligned keyboard shortcut and an optional leading widget (a CheckBox, a RadioButton, an Image, …).

const auto menu = uiWindow->create<lysa::ui::Menu>(lysa::ui::Alignment::FILL);
const auto open = menu->addItem("Open", "Ctrl+O");
menu->addItem("Save", "Ctrl+S");
menu->addSeparator();
// Leading widget : a check box reflecting a setting
const auto grid = std::make_shared<lysa::ui::CheckBox>();
const auto item = menu->addItem("Show grid", "", "", "", grid);
// Restyle an item & its shortcut after creation
menu->getItem(open)->setTextColor(lysa::float4{1.0f, 1.0f, 1.0f, 1.0f});
menu->getItemShortcut(open)->setTextColor(lysa::float4{0.6f, 0.6f, 0.6f, 1.0f});
// Highlight the current entry, Menu::NO_ITEM removes the highlight
menu->highlight(item);

A menu does not size itself : ask it for the room its items need, typically to size the Window or the Popup holding it :

holder->setSize(menu->getContentWidth(), menu->getContentHeight());
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnSelectItem, menu->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventItem>(e.payload);
runCommand(payload.index); // index of the clicked item, separators excluded
});

20. TabBar widget

TabBar draws a horizontal row of tabs and tracks which one is selected. It does not manage what the tabs display : subscribe to UIEvent::OnSelectItem and manage the content yourself.

const auto bar = panel->create<lysa::ui::TabBar>(lysa::ui::Alignment::TOP);
bar->addTab("Scene");
bar->addTab("Assets");
// An icon can be displayed at the left of the label
bar->addTab("Log", "", std::make_shared<lysa::ui::CrossMark>());
bar->setSize(0.0f, bar->getContentHeight()); // width is filled by the TOP alignment
// The first tab added is selected without emitting UIEvent::OnSelectItem
bar->select(1);
const auto index = bar->getSelected(); // TabBar::NO_TAB when empty
bar->getTab(index)->setTextColor(lysa::float4{1.0f, 1.0f, 0.0f, 1.0f});
bar->removeTab(0);
bar->removeAllTabs();

The tabs open downwards by default. Pass TabPlacement::BOTTOM to the constructor for a bar sitting below its content, so the tabs open upwards :

lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnSelectItem, bar->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventItem>(e.payload);
showPage(payload.index); // payload.item is the Text widget of the tab
});

21. TabContainer widget

TabContainer arranges widgets into a tabbed view : it creates a tab for each widget added and displays only the one of the selected tab and owns a TabBar.

const auto tabs = panel->create<lysa::ui::TabContainer>(
"width=400;height=300", lysa::ui::Alignment::FILL);
// Add an existing widget, or create the widget of a tab in one step
const auto scene = tabs->addTab("Scene", std::make_shared<lysa::ui::Panel>());
const auto props = tabs->createTab<lysa::ui::Box>("Properties");
// An icon & a resource string for the label can be passed too
tabs->addTab("Log", std::make_shared<lysa::ui::ScrollBox>(), "", std::make_shared<lysa::ui::CheckMark>());
// The widget of the first tab added is displayed right away
tabs->select(1);
const auto shown = tabs->getSelectedContent(); // nullptr when there is no tab
const auto index = tabs->getIndexOf(props); // TabContainer::NO_TAB when not found
tabs->getTab(index)->setText("Inspector"); // the Text widget of a tab
tabs->removeTab(0); // removes the tab & the widget it displays
tabs->removeAllTabs();

The bar of tabs adjusts its height to the tabs, set an explicit one to override it::

tabs->setTabBarHeight(24.0f); // zero restores the automatic height
tabs->setPlacement(lysa::ui::TabPlacement::BOTTOM);
// Both internal widgets stay reachable
tabs->getTabBar()->setTabPadding(10.0f);
tabs->getContentArea()->setPadding(4.0f);

Selecting a tab emits UIEvent::OnSelectItem on the container :

lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnSelectItem, tabs->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventItem>(e.payload);
if (payload.item != nullptr) { refreshPage(payload.item); }
});

22. FoldableContainer widget

FoldableContainer groups widgets under a clickable title bar folding & unfolding them. It adjusts its own height to its title bar when folded & to its title bar plus its content when unfolded : stack several of them with Alignment::TOP to build a list of collapsible groups.

const auto group = panel->create<lysa::ui::FoldableContainer>(
lysa::ui::Alignment::TOP, "Transform");
// Widgets must be added with addContent()/createContent() : the container
// recomputes its height on each call
const auto row = group->createContent<lysa::ui::Widget>("height=24", lysa::ui::Alignment::TOP);
group->addContent(std::make_shared<lysa::ui::Text>("0.0, 0.0, 0.0"));
group->removeContent(row);
group->removeAllContent();

The heights of the title bar & of the content are computed from the title & from the widgets of the content, set explicit ones to override them :

group->setTitleBarHeight(24.0f); // zero restores the automatic height
group->setContentHeight(120.0f); // zero restores the automatic height
group->updateSize(); // only needed after resizing the widgets of the content

The state is read & changed from the code, & the internal widgets stay reachable :

group->setFolded(); // hides the content
group->toggle();
if (group->isFolded()) { /* ... */ }
group->getTitleWidget()->setTextColor({1.0f, 1.0f, 1.0f, 1.0f});
group->getTitleBar()->setPadding(2.0f);
group->getContent()->setPadding(2.0f);

Clicking the title bar emits UIEvent::OnExpandItem on the container :

lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnExpandItem, group->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventExpand>(e.payload);
savePreference(payload.expanded); // payload.item is the content widget
});

23. Slider widget

Slider (and its aliases HSlider / VSlider) lets the user drag a grip along a track to select a value within a numeric range:

// Horizontal slider : range 0-100, initial value 50, step 1
const auto slider = panel->create<lysa::ui::HSlider>(
"width=200;height=20", lysa::ui::Alignment::TOP,
0.0f, 100.0f, 50.0f, 1.0f);
[slider](const lysa::Event&) {
setVolume(slider->getValue());
});
slider->setGripSize(18.0f); // length of the draggable grip
slider->setTrackSize(4.0f); // thickness of the track
slider->getTrack()->setPadding(0.0f);
slider->getFill()->setDrawBackground(false); // hides the filled part
slider->setResources("style=LOWERED", "style=FLAT;color=0.2,0.5,0.9,1.0", "style=RAISED");

24. Knob widget

Knob selects a value by rotating a disc. A cursor drawn inside the disc points at the current value, and the values of the graduations can be drawn around it:

// Range 0-100, initial value 50, step 1
const auto knob = panel->create<lysa::ui::Knob>(
"width=" + std::to_string(80.0f / panel->getAspectRatio()) + ";height=80",
0.0f, 100.0f, 50.0f, 1.0f);
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnValueChange, knob->id,
[knob](const lysa::Event&) {
setGain(knob->getValue());
});
knob->setStartAngle(180.0f); // minimum at the left
knob->setSweepAngle(180.0f); // maximum at the right, half a turn clockwise

The graduations are drawn outside of the disc, their values only on demand:

knob->setDivisions(4); // five graduations : min, max & three in between
knob->setLabelsVisible(); // draws the value of each graduation
knob->setPrecision(1); // number of decimals of the labels
knob->setTicksVisible(false); // hides the graduations, keeps the labels

The disc is drawn RAISED by default, and the cursor, the graduations and the labels use the text color of the style :

knob->setTickLength(6.0f); // length of the graduations
knob->setTickWidth(2.0f); // thickness of the graduations
knob->setTickGap(3.0f); // space between the disc & the graduations
knob->setLabelGap(3.0f); // space between the graduations & the labels
knob->setCursorWidth(3.0f); // thickness of the cursor
knob->setCursorExtent(0.0f, 0.9f); // cursor from the center to 90% of the radius

25. SpinBox widget

SpinBox displays a numeric value in an editable text field :

const auto spin = panel->create<lysa::ui::SpinBox>(
"width=100;height=24", lysa::ui::Alignment::TOP,
0.0f, 255.0f, 128.0f, 1.0f);
spin->setPrecision(2); // number of decimals, zero for an integer value
spin->setButtonsWidth(14.0f); // width of the buttons column
spin->setButtonsPlacement(lysa::ui::SpinBox::LEFT); // buttons column at the left of the field
spin->setReadOnly(); // the value can no longer be typed in
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnValueChange, spin->id,
[spin](const lysa::Event&) {
applyOpacity(spin->getValue());
});
spin->setRepeatDelay(400); // milliseconds before the repeat starts
spin->setRepeatInterval(60); // milliseconds between two repeats
spin->getTextEdit()->setReadOnly();
spin->getUpButton()->setPadding(0.0f);
spin->setResources("style=LOWERED");

The column of the two buttons is a SpinButtons widget also usable alone :

const auto buttons = panel->create<lysa::ui::SpinButtons>(
"width=14;height=24", lysa::ui::Alignment::RIGHT,
lysa::ui::SpinButtons::VERTICAL); // or HORIZONTAL : decrement at the left, increment at the right
buttons->setRepeatDelay(400); // milliseconds before the repeat starts
buttons->setRepeatInterval(60); // milliseconds between two repeats, zero to disable the repeat
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnClick, buttons->id,
[](const lysa::Event& event) {
const auto& step = std::any_cast<const lysa::ui::UIEventStep&>(event.payload);
scroll(step.steps); // +1.0 for the up button, -1.0 for the down button
});

26. DropDownList widget

DropDownList displays the selected item of a list of strings & opens a popup list:

const auto list = panel->create<lysa::ui::DropDownList>(
"width=180;height=24", lysa::ui::Alignment::TOP);
list->setItems({"Debug", "Release", "RelWithDebInfo"}, "Release");
const auto index = list->addItem("MinSizeRel");
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnSelectItem, list->id,
[list](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventItem>(e.payload);
selectConfiguration(payload.index, list->getSelectedItem());
});

The selection is read & changed by index or by label (NO_ITEM (-1) meaning no selection) :

list->select(2);
list->select("Debug");
if (list->getSelectedIndex() == lysa::ui::DropDownList::NO_ITEM) { /* ... */ }
list->setPlaceHolder("Select a configuration"); // displayed when nothing is selected
list->removeAllItems();

The popup list is opened & closed from the code :

list->open();
list->close();
if (list->isOpen()) { /* ... */ }
list->setMaxDisplayedItems(6); // the list scrolls beyond that
list->setArrowWidth(14.0f);
list->setResources("style=RAISED", "", "style=LOWERED");

27. IconImage and IconSVG widgets

Icon is the base of the widgets drawing a picture fitted into the widget rectangle, keeping the aspect ratio of the source : IconImage for a 2D image, IconSVG for a vector shape.

const auto icon = panel->create<lysa::ui::IconSVG>(
"width=16;height=16", lysa::ui::Alignment::LEFTCENTER,
"app://res/icons/build.svg");
icon->setColor({0.9f, 0.9f, 0.9f, 1.0f}); // hasCustomColor() is now true
icon->setScale(0.8f); // shrinks the shape inside the rectangle
icon->resetColor(); // reverts to the color of the style

The shape of an IconSVG is loaded from a URI :

icon->setSVG("app://res/icons/run.svg", 2.5f, 0.002f, lysa::SVGBounds::CONTENT);
lysa::ui::IconSVG::clearCache(); // frees every cached shape

An IconImage displays a GPU image :

const auto bitmap = panel->create<lysa::ui::IconImage>(
"width=24;height=24", lysa::ui::Alignment::LEFTCENTER);
bitmap->setImage("app://res/icons/logo.png");
Note
The size of an icon is reset when it is added to a parent since the style applies the size of its resource string. Give the size in the resource string or call setSize() after adding it.

28. ColorWheel and ColorSlider widgets

ColorWheel is a disc selecting the hue & the saturation of a color:

const auto wheel = panel->create<lysa::ui::ColorWheel>(
"width=150;height=150", lysa::ui::Alignment::TOP);
wheel->setColor({1.0f, 0.5f, 0.0f, 1.0f});
wheel->setHueSaturation(210.0f, 0.75f); // hue in degrees, saturation in [0, 1]
wheel->setBrightness(0.8f);
wheel->setAlpha(0.5f);
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnColorChange, wheel->id,
[](const lysa::Event& e) {
const auto& payload = std::any_cast<lysa::ui::UIEventColor>(e.payload);
applyColor(payload.color); // payload.previous holds the color before the change
});

The conversions used by the wheel are available as static helpers:

const auto rgba = lysa::ui::ColorWheel::fromHSV(210.0f, 0.75f, 0.8f);
const auto hsv = lysa::ui::ColorWheel::toHSV(rgba); // x = hue, y = saturation, z = brightness
wheel->setSegments(360); // angular subdivisions
wheel->setRings(48); // concentric subdivisions
wheel->setCursorRadius(4.0f);

ColorSlider (aliases HColorSlider / VColorSlider) is a Slider whose track is drawn with a gradient of colors & whose value is marked by an arrow :

const auto ramp = panel->create<lysa::ui::HColorSlider>(
"width=200;height=16", lysa::ui::Alignment::TOP,
0.0f, 255.0f, 128.0f, 1.0f);
ramp->setColors({{0.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 0.0f, 1.0f}, {1.0f, 1.0f, 1.0f, 1.0f}});
ramp->setSteps(64); // quads used to draw the gradient
const auto middle = ramp->colorAt(0.5f); // interpolated color at a ratio in [0, 1]

29. ColorPicker widget

ColorPicker assembles a preview box, a ColorWheel with its brightness slider & one row per component into a complete color selector :

const auto picker = panel->create<lysa::ui::ColorPicker>(
lysa::float4{0.2f, 0.6f, 1.0f, 1.0f});
picker->setColor({1.0f, 0.0f, 0.0f, 1.0f});
const auto& color = picker->getColor();
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnColorChange, picker->id,
[](const lysa::Event& e) {
applyColor(std::any_cast<lysa::ui::UIEventColor>(e.payload).color);
});

The labels of the tabs can replaced for localization :

picker->setMode(lysa::ui::ColorPicker::HSV); // RGB, HSV or LINEAR
picker->setModeLabels({"RVB", "TSV", "Linéaire"});
picker->setTabHeight(24.0f);

The picker does not size itself : ask it for the height it needs to size the Window or the container holding it :

window->setHeight(picker->getAutoHeight());
picker->setPreviewHeight(32.0f);
picker->setWheelHeight(150.0f);
picker->setBrightnessWidth(16.0f);
picker->setRowHeight(28.0f);
picker->setLabelWidth(16.0f);
picker->setValueWidth(60.0f);
picker->setFooterHeight(24.0f); // zero hides the footer
picker->getWheel()->setCursorRadius(5.0f);
picker->getSlider(lysa::ui::ColorPicker::ALPHA)->setVisible(false);
picker->getValue(lysa::ui::ColorPicker::RED)->setPrecision(0);
picker->getFooter()->create<lysa::ui::Text>(lysa::ui::Alignment::CENTER, "#FF0000");

When the colors of the application are stored in the linear space give the picker a transform used for the preview & the gradients only :

picker->setPreviewTransform([](const lysa::float4& c) {
return lysa::float4{
c.a};
});

30. ColorPickerButton widget

ColorPickerButton is a button displaying a color swatch :

lysa::float4{1.0f, 1.0f, 1.0f, 1.0f}, "Ambient light");
[](const lysa::Event& e) {
setAmbientColor(std::any_cast<lysa::ui::UIEventColor>(e.payload).color);
});

The dialog is described by a ColorPickerButton::Dialog struct letting you localize its title, its buttons & adjust its geometry :

button->setDialog({
.title = "Ambient light",
.buttons = {"Select", "Cancel"},
.width = 170.0f,
.footerHeight = 0.0f,
dialog.getColorPicker()->setMode(lysa::ui::ColorPicker::HSV);
},
});
button->setColor({0.5f, 0.5f, 0.5f, 1.0f});
button->setLabel("Fog color");
button->setSwatchWidth(48.0f); // width of the color swatch inside the button

31. Modal dialogs

ModalDialog is a Window : it displays a DialogBox centered on the screen, above its parent, and grabs the focus until it is closed. It needs a parent widget or window to find the WindowManager it must be attached to:

const auto dialog = std::make_shared<lysa::ui::ModalDialog>(
"Quit", std::vector<std::string>{"OK", "Cancel"}, 300.0f, 120.0f);
dialog->add(*panel); // or dialog->add(*uiWindow)
dialog->show();
// The buttons of the DialogBox fire OnSelectItem with their index
lysa::ctx().events.subscribe(lysa::ui::UIEvent::OnSelectItem, dialog->getDialog().id,
[dialog](const lysa::Event& event) {
const auto button = std::any_cast<const lysa::ui::UIEventItem&>(event.payload).index;
lysa::ctx().defer.push([dialog, button] {
if (button == 0) { /* ... */ }
dialog->hide();
});
});

TextInputDialog asks the user for a single line of text. The validation callback is called when the action button is pressed and returns an empty string to accept the input or the message to display below the field to reject it:

const auto rename = std::make_shared<lysa::ui::TextInputDialog>(
"Rename",
std::vector<std::string>{"Rename", "Cancel"},
[](const std::string& text) -> std::string {
if (text.empty()) { return "The name can not be empty"; }
return "";
});
rename->setText("current name");
rename->add(*panel);
rename->show();

The dialog closes itself when the input is accepted, when it is unchanged, or when the cancel button (TextInputDialog::BUTTON_CANCEL) is pressed. getTextEdit() returns the TextEdit and getMessage() the Text displaying the validation message.

ColorModalDialog is the dialog opened by a ColorPickerButton it wraps aColorPicker` and calls back with the selected color.


32. Applying a style

The WindowManager creates a StyleClassic at construction and hands it to every Window that does not set its own. Reconfigure it through the manager to restyle the whole UI:

auto& style = windowManager.getDefaultStyle();
style.setOption("color_focus", "0.3,0.5,0.9,1.0");
style.setOption("color_highlight", "0.3,0.5,0.9,1.0");
style.setOption("color_foreground_up", "0.40,0.60,0.70,1.0");
style.setOption("color_foreground_down", "0.65,0.86,0.86,1.0");
style.setOption("color_shadow_dark", "0.1,0.1,0.1,1.0");
style.setOption("color_shadow_bright", "0.9,0.9,0.9,1.0");
style.setOption("color_tree_lines", "0.1,0.1,0.1,1.0");
style.setOption("color_text", "1.0,1.0,1.0,1.0");

color_text is the default color of the texts returned by Style::getTextColor() and applied to every Text and RichTextArea that was not given an explicit color.

Style also exposes the default size of the widgets the style sizes itself :

style.setOption("size_scroll_bar", "18");
style.setOption("size_check_box", "16");
style.setOption("size_radio_button", "16");
style.setOption("size_tree_indent", "12");

The other built-in look is StyleMaterial, inspired by Material Design : flat tonal surfaces,

uiWindow->setStyle(lysa::ui::Style::create("material"));
auto& material = uiWindow->getStyle();
material.setOption("color_primary", "0.816,0.737,1.0,1.0");
material.setOption("color_surface", "0.078,0.071,0.094,1.0");
material.setOption("color_outline", "0.576,0.561,0.600,1.0");
material.setOption("size_corner_radius", "8");
uiWindow->setStyle(std::make_shared<MyStyle>());
uiWindow->setStyle(nullptr);

Widget resource strings are parsed by the active style. For the built-in styles, a resource string can encode the desired size and appearance:

// "width=W;height=H" size only, default RAISED
// "width=W;height=H;style=FLAT" flat appearance
// "width=W;height=H;style=LOWERED" lowered appearance
// "width=W;height=H;color=r,g,b,a" custom background color (0.0–1.0)
const auto box = panel->create<lysa::ui::Box>("width=120;height=40", lysa::ui::Alignment::CENTER);
const auto flat = panel->create<lysa::ui::Box>("width=120;height=40;style=FLAT", lysa::ui::Alignment::CENTER);
const auto sunken = panel->create<lysa::ui::Box>("width=120;height=40;style=LOWERED", lysa::ui::Alignment::CENTER);
const auto tinted = panel->create<lysa::ui::Box>("width=120;height=40;color=0.78,0.39,0.20,1.0", lysa::ui::Alignment::CENTER);

33. Show, hide, and enable

Widgets are shown and hidden with setVisible(); a Window additionally offers the show() and hide() aliases, and its visibility changes take effect at the start of the next frame:

// Widgets
statusLabel->setVisible(); // true by default
debugPanel->setVisible(false);
// Windows
hud->show();
pauseMenu->hide();
pauseMenu->setVisible(!pauseMenu->isVisible());

Widget::isVisible() walks the whole parent chain and the owning Window.

Disabling a widget prevents it from responding to mouse and keyboard events but keeps it visible:

submitButton->setEnabled(false);
// ...
submitButton->setEnabled(true);

Force an "immediate" repaint of a widget and its children (widgets are drawn at the start of each frame):

panel->refresh();