Skip to content

UI ​

Retained-mode UI widget system with theming, layouts, focus navigation, and input handling.

Header: #include <asw/modules/ui/ui.h>
Namespace: asw::ui

Ui example
Runs in your browser. Keyboard and mouse recommended.Open in new tab ↗

From the ui example.

Widget ​

Base class for all UI widgets. Provides identity, visibility, focus, parent/child tree management, and virtual methods for layout, event handling, activation, and drawing.

cpp
class Widget;

Fields ​

FieldTypeDefaultDescription
visiblebooltrueWhether the widget is visible
enabledbooltrueWhether the widget is enabled
focusableboolfalseWhether the widget can receive focus. Only focusable widgets can be pressed with the pointer
nav_up / nav_down / nav_left / nav_rightWidget*nullptrWidget to focus in that direction, instead of the nearest one
focus_ringbooltrueDraw the theme focus ring while the widget has visible focus. Turn off for widgets that show focus themselves
parentWidget*nullptrPointer to the parent widget
transformasw::Quad<float>Position and size
anchorAnchorNonePin the widget to a point of its parent, e.g. BottomRight for a corner button. The parent places it at each layout, so it stays in place when the screen or its own size changes. Stack and Grid place their children themselves, and ignore it
anchor_marginasw::Vec2<float>{0, 0}Space between an anchored widget and the edges of its parent

Anchor Enum ​

cpp
enum class Anchor {
  None,
  TopLeft, Top, TopRight,
  Left, Center, Right,
  BottomLeft, Bottom, BottomRight,
};
cpp
auto& settings = ui.root.add_child<asw::ui::Button>();
settings.set_text("Settings", true);
settings.anchor = asw::ui::Anchor::BottomRight;
settings.anchor_margin = {20.0f, 20.0f};

Methods ​

is_hovered / is_pressed / is_focused / is_captured ​

cpp
bool is_hovered() const;
bool is_pressed() const;
bool is_focused() const;
bool is_captured() const;

Get the interaction state of the widget: the pointer is over it, it is being pressed, it holds focus, or it was pressed and the pointer is still held (even off the widget). Root and FocusManager set these states.

is_highlighted ​

cpp
bool is_highlighted(const Context& ctx) const;

Returns true if the widget should draw its hover look: it is hovered, pressed, or focused while focus is shown.

id ​

cpp
WidgetId id() const;

Get the unique identifier for this widget (WidgetId is uint32_t).

children ​

cpp
const std::vector<std::unique_ptr<Widget>>& children() const;

Get the child widgets. Change them with add_child, remove_child and clear_children.

Changed in v0.12.2

children was a public field. It is now private. Read it with children().

measure ​

cpp
virtual void measure(Context& ctx);

Set this widget's own size, before its parent places it. Does nothing by default. Override it for a widget whose size depends on the context, e.g. on the theme font. Button uses it to fit its text.

layout ​

cpp
virtual void layout(Context& ctx);

Lay out this widget and its children.

on_event ​

cpp
virtual bool on_event(Context& ctx, const UIEvent& e);

Handle a UI event. Returns true if the event was consumed.

on_focus_changed ​

cpp
virtual void on_focus_changed(Context& ctx, bool focused);

Called when focus state changes.

activate ​

cpp
virtual void activate(Context& ctx);

Called when the widget is clicked, or activated with the keyboard or a controller while focused. Does nothing by default. Override this for custom widgets, instead of handling clicks in on_event.

draw ​

cpp
virtual void draw(Context& ctx);

Draw this widget and its children.

add_child ​

cpp
template <class T, class... Args>
T& add_child(Args&&... args);

Add a child widget of type T (must derive from Widget). Returns a reference to the new child.

remove_child / clear_children ​

cpp
bool remove_child(const Widget& child);
void clear_children();

Remove one child, or all children. remove_child returns false if the widget is not a child of this widget. A removed widget leaves the tree at once, and is destroyed at the next Root::update(). Thus a callback can remove any widget, even the widget that it runs in.

Moving a widget ​

A moved widget takes the children with it, and they point to it as their parent. A move-constructed widget gets a new id and no parent. A move-assigned widget keeps its place in the tree: its parent, id and state. Its old children are removed as with clear_children.

UIEvent ​

Event structure for UI interactions.

cpp
struct UIEvent;

Event Types ​

cpp
enum class Type {
  KeyDown, KeyUp, TextInput,
  PointerDown, PointerUp, PointerMove,
  PointerEnter, PointerLeave,
  Back
};

There is no Activate event. Root calls Widget::activate() instead.

Fields ​

FieldTypeDescription
typeTypeThe event type
keyasw::input::KeyKey associated with keyboard events
pointer_idintPointer identifier
pointer_posasw::Vec2<float>Pointer position
mouse_buttonasw::input::MouseButtonMouse button for pointer events
textstd::stringText for TextInput events

Theme ​

Visual style for all UI elements. Each widget type has a style struct. Set the struct on the theme to change every widget of that type, or set the widget's own style to change one widget.

cpp
struct Theme;
FieldTypeDefaultDescription
textasw::ColorwhiteDefault text color
text_dimasw::Colorlight grayDimmed text color
panel_bgasw::Colordark grayPanel background
fontasw::FontnullptrFont for widgets that do not set their own
buttonButtonStyleDefault button style
checkboxCheckboxStyleDefault checkbox style
inputInputStyleDefault input box style
sliderSliderStyleDefault slider style
focus_ringFocusRingStyleFocus ring style
toastToastStyleStyle of messages from Root::toast
modalModalStyleStyle of modals from Root::open_modal
paddingfloat10.0Default padding
gapfloat8.0Default gap between elements
sound_moveasw::SamplenullptrPlayed on the Ui sound bus when navigation moves focus
sound_activateasw::SamplenullptrPlayed on the Ui sound bus when a widget is clicked or activated

ButtonStyle ​

Used by Button and Choice.

FieldTypeDefaultDescription
bgasw::Color{55, 55, 55}Background color
bg_hoverasw::Color{75, 75, 75}Background while hovered, or focused with focus shown
bg_pressedasw::Color{95, 95, 95}Background while pressed
bg_disabledasw::Color{30, 30, 30}Background while disabled
textasw::ColorwhiteText color
text_hoverasw::ColorwhiteText color while hovered, or focused with focus shown
text_disabledasw::Color{200, 200, 200}Text color while disabled
borderasw::ColortransparentBorder color
border_widthfloat0Border width in pixels, drawn inside the button. 0 draws no border
text_alignasw::TextJustifyCenterWhere the text sits. Left and right are inset by the button padding

CheckboxStyle ​

FieldTypeDefaultDescription
bg / bg_hoverasw::ColortransparentBackground of the whole row, normal and highlighted
borderasw::ColortransparentRow border color
border_widthfloat0Row border width. 0 draws no border
boxasw::Color{55, 55, 55}Box color
box_hoverasw::Color{75, 75, 75}Box color while highlighted
box_pressedasw::Color{95, 95, 95}Box color while pressed
box_disabledasw::Color{30, 30, 30}Box color while disabled
box_borderasw::ColortransparentBox border color
box_border_widthfloat0Box border width. 0 draws no border
mark / mark_disabledasw::Colorwhite / {200, 200, 200}Check mark color, normal and disabled
text / text_disabledasw::Colorwhite / {200, 200, 200}Text color, normal and disabled
box_sideBoxSideLeftSide of the box (BoxSide::Left or BoxSide::Right). The text is on the other side

InputStyle ​

FieldTypeDefaultDescription
bgasw::Color{20, 20, 20}Background color
bg_disabledasw::Color{30, 30, 30}Background while disabled
borderasw::Color{55, 55, 55}Border color
border_hoverasw::Color{75, 75, 75}Border color while hovered
border_widthfloat1Border width. 0 draws no border
textasw::ColorwhiteText color
placeholderasw::Color{200, 200, 200}Placeholder text color
caretasw::ColorwhiteText cursor color

SliderStyle ​

FieldTypeDefaultDescription
trackasw::Color{55, 55, 55}Track color
fillasw::Color{255, 200, 80}Track color up to the value
knobasw::Color{200, 200, 200}Knob color
knob_hoverasw::ColorwhiteKnob color while hovered, pressed, or focused with focus shown
knob_editingasw::Color{255, 200, 80}Knob color while in edit mode
disabledasw::Color{90, 90, 90}Knob and fill color while disabled
track_heightfloat4Track height in pixels, centred in the widget
knob_widthfloat10Knob width in pixels. The knob is as tall as the widget

FocusRingStyle ​

The ring drawn around the focused widget during keyboard or controller navigation.

FieldTypeDefaultDescription
colorasw::Color{255, 200, 80}Ring color
widthfloat1Ring width in pixels. 0 draws no ring
offsetfloat2Gap between the widget and the ring, in pixels

ToastStyle ​

FieldTypeDefaultDescription
bgasw::Color{20, 20, 20, 230}Fill behind each message
textasw::ColorwhiteMessage text color
border / border_widthasw::Color / floatnone / 0Outline. No outline when the width is 0
paddingfloat12Space around the text in each message
marginfloat16Space from the edge of the screen to the first message
gapfloat8Space between messages
secondsfloat3How long a message stays, from when it shows
fade_secondsfloat0.25How long a message takes to fade out at the end
max_visiblestd::size_t3Messages on screen at the same time. The others wait
anchorAnchorTopWhere messages show. Top, TopLeft and TopRight stack down from the top. Bottom, BottomLeft and BottomRight stack up from the bottom

ModalStyle ​

FieldTypeDefaultDescription
dimasw::Color{0, 0, 0, 160}Color over the screen behind the modal
bgasw::Color{30, 30, 30}Fill of the modal
border / border_widthasw::Color / floatnone / 0Outline. No outline when the width is 0
paddingfloat20Space between the edge of the modal and its content
gapfloat12Space between its widgets
min_widthfloat320Smallest width of the modal

Helpers ​

cpp
const asw::Font& pick_font(const asw::Font& own, const Theme& theme);
void draw_focus_ring(const FocusRingStyle& style, const asw::Quad<float>& bounds);

pick_font returns the widget's own font, or the theme font if the widget has none. draw_focus_ring draws a focus ring around bounds. Use them in custom widgets.

Action names the UI reads for focus navigation. See Action.

cpp
struct Navigation;
FieldDescription
up / down / left / rightMove focus in a direction
nextMove focus to the next widget. Hold Shift to go back, so Shift+Tab works when Tab is bound here
prevMove focus to the previous widget
activatePress the focused widget
backGo back. Sent to the focused widget, then to Root::on_back

A key that types text into an InputBox does not also navigate, activate or go back.

An empty name uses the built-in keys for that step: arrows, Tab and Shift+Tab, Enter or Space, and Escape. Set a name to read that action instead, so the UI follows the game's own bindings, controllers included.

bind_default_navigation ​

cpp
Navigation bind_default_navigation(std::string_view prefix = "ui_");

Bind keyboard and controller actions for UI navigation and return their names. Set the result on Context::navigation. If you call it again, it replaces the bindings. For any controller, it binds:

ActionBindings
up, down, left, rightArrow keys, D-pad, and left stick
nextTab and right shoulder
prevLeft shoulder
activateEnter, Space, and A
backEscape and B
cpp
// Default keyboard and controller bindings
ui.ctx.navigation = asw::ui::bind_default_navigation();

// Or the game's own actions
ui.ctx.navigation.up = "move_up";
ui.ctx.navigation.activate = "jump";

Context ​

Shared state for the UI system.

cpp
class Context;
FieldTypeDescription
themeThemeThe current UI theme
focusFocusManagerFocus navigation manager
navigationNavigationActions to read for focus navigation
show_focusboolWhether focus is shown. Root turns it on for keyboard and controller navigation and off when the mouse is used

FocusManager ​

Manages keyboard and directional focus navigation.

cpp
class FocusManager;
MemberDescription
default_focusWidget* focused by the first navigation press when nothing has focus. If nullptr, the first focusable widget is used
focused()Get the currently focused widget (or nullptr)
set_focus(Context& ctx, Widget* w)Set focus to a specific widget
focus_next(Context& ctx)Move focus to the next widget
focus_prev(Context& ctx)Move focus to the previous widget
focus_start(Context& ctx)Focus default_focus, or the first focusable widget, when nothing has focus. Returns true if focus was set
focus_dir(Context& ctx, int dx, int dy)Move focus in a 2D direction

A hidden or disabled widget cannot take focus, and neither can its children.

focus_dir picks the target in this order:

  1. The widget's nav_up, nav_down, nav_left or nav_right, if set.
  2. Widgets in the same lane (they overlap across the direction of the move) before other widgets.
  3. The nearest widget, edge to edge.
  4. The widget closest to the lane where repeated moves in the same direction started.

Root ​

Top-level manager that owns the UI tree, reads input, and draws. Call update() once per frame after asw::core::update(), then draw(). Root tracks hover, press, focus and the focus ring for every widget.

cpp
class Root;

Fields ​

FieldTypeDefaultDescription
ctxContextThe UI context
rootPanelThe root panel widget. It is transparent. Set root.bg for a background
on_backstd::function<void()>Called when back is pressed and the focused widget does not use it, e.g. to leave a menu
auto_sizebooltrueKeep the root panel the size of the logical screen. set_size turns it off

Methods ​

MethodDescription
set_size(float w, float h)Set a fixed size for the root panel
update()Process input for this frame. Returns true if the UI used input: the pointer is over or pressing a widget, or a navigation, activate, back or text key went to the UI. Skip your game's own input for that frame when it returns true
draw()Draw the UI tree, then the focus ring
focus(Widget& w, bool show = false)Focus a widget. It must be focusable and in this tree. If show is true, the focus ring is shown, as with keyboard navigation
clear_focus()Remove focus from every widget
toast(const std::string& text)Show a message for a few seconds, e.g. "Achievement unlocked". Messages wait in a queue when many arrive together
toast(Toast message)Show a message with its own icon, color or time
clear_toasts()Remove every message, shown or waiting
toast_count()Get the number of messages, shown or waiting
open_modal<T = Modal>(args...)Open a modal over the rest of the UI, and return it
has_modal()Returns true while a modal is open
close_modals()Close every open modal, top first
cpp
if (!ui.update()) {
  // The UI did not use input this frame
  player.update();
}

Toast ​

cpp
struct Toast;
FieldTypeDefaultDescription
textstd::stringThe message
iconasw::TexturenullptrImage before the text, e.g. an achievement icon
secondsfloat0How long the message stays. 0 uses the theme toast.seconds
colorstd::optional<asw::Color>Text color. Uses the theme toast.text when empty

Toasts use real time, so they fade out at the same speed when the game is paused.

cpp
ui.toast("Saved");
ui.toast({.text = "Achievement unlocked", .icon = trophy, .seconds = 5.0f});

Layouts ​

Stack ​

Places children one after another, top to bottom or left to right. Children keep their own size along the stack, for example the height of each row in a vertical stack. Across the stack, they follow align. Hidden children take no space. Put stacks in stacks to make rows inside columns. When Stretch stops forcing a size on a child, e.g. after align changes, the child gets its own size back.

cpp
class Stack : public Widget;
FieldTypeDefaultDescription
directionDirectionVerticalDirection::Vertical or Direction::Horizontal
alignAlignStretchWhere children sit across the stack: Start, Center, End, or Stretch to fill
gapfloat8.0Space between children
paddingfloat0.0Space inside the stack on every side

VBox ​

A vertical Stack with padding set to 10.0. It is now in layout.h. The old vbox.h header still works.

cpp
class VBox : public Stack;

Grid ​

Places children in a grid and fills each row left to right, e.g. a level select or a 2 by 2 menu. All cells have the same width, set by columns. Children fill their cell. Hidden children take no cell.

cpp
class Grid : public Widget;
FieldTypeDefaultDescription
columnsstd::size_t2Number of columns
gapfloat8.0Space between cells, both ways
paddingfloat0.0Space inside the grid on every side
row_heightfloat0.0Height of every row. 0 makes each row as tall as its tallest child

A child that the row made taller gets its own height back when the row stops forcing it, e.g. after the tallest child in the row is hidden.

in_row_with_focusables ​

cpp
bool in_row_with_focusables(const Widget& w);

Returns true if the widget's parent is a horizontal Stack, or a Grid with more than one column, that holds another visible, enabled, focusable child. Choice and Slider use this to keep left and right for moving focus in the row.

Widgets ​

Panel ​

A container widget with an optional background color or image.

cpp
class Panel : public Widget;
FieldTypeDefaultDescription
bgasw::ColortransparentBackground color. A fully transparent bg is not drawn
bg_imageasw::TexturenullptrBackground image texture

Label ​

A non-focusable text display widget.

cpp
class Label : public Widget;
FieldTypeDefaultDescription
fontasw::FontFont. Uses the theme font when empty
textstd::stringText to display
justifyasw::TextJustifyLeftText justification
colorstd::optional<asw::Color>Text color. Uses the theme text color when empty
MethodDescription
set_text(const std::string& t, bool auto_size = false)Set the text. If auto_size is true, the label is sized to its text when it is measured, so layouts such as VBox and Modal give it room

Image ​

A texture stretched over the widget, e.g. a logo or icon in a layout.

cpp
class Image : public Widget;
MemberDescription
textureThe asw::Texture to draw
set_texture(const asw::Texture& tex, bool auto_size = true)Set the texture. If auto_size is true, resize the widget to the texture size

Button ​

An interactive button widget (focusable).

cpp
class Button : public Widget;
FieldTypeDescription
on_clickstd::function<void()>Callback invoked on a left click, or when the button is activated with the keyboard or a controller
paddingfloatPadding inside the button on all sides (default: 0)
fontasw::FontFont for the button text. Uses the theme font when empty
textstd::stringButton label text
textureasw::TextureTexture to display on the button
texture_hoverasw::TextureTexture while hovered, or focused with focus shown. Falls back to texture
texture_pressedasw::TextureTexture while pressed. Falls back to texture_hover, then texture
texture_disabledasw::TextureTexture while disabled. Falls back to texture
draw_backgroundboolFill the button with the style background (default: true). Turn off for image-only buttons
stylestd::optional<ButtonStyle>Style for this button only. Uses the theme button style when empty
MethodDescription
set_text(const std::string& t, bool auto_size = false)Set the text. If auto_size is true, resize the button to fit the text. A button without its own font is sized with the theme font when it is next measured, before its parent places it
set_texture(const asw::Texture& tex, bool auto_size = false)Set the texture. If auto_size is true, resize the button to the texture size
set_images(normal, hover = nullptr, pressed = nullptr, disabled = nullptr, bool auto_size = true)Make an image button: set the state textures, turn off the background, and optionally resize the button to the normal texture
get_style(const Context& ctx)Get the style the button draws with: its own style, or the theme button style

An image button that swaps to a second image on hover:

cpp
auto& play = root.add_child<asw::ui::Button>();
play.set_images(asw::assets::load_texture("assets/play.png"),
                asw::assets::load_texture("assets/play_hover.png"));
play.on_click = []() { /* start the game */ };

A button with its own style:

cpp
asw::ui::ButtonStyle danger;
danger.bg = {120, 40, 40};
danger.bg_hover = {160, 60, 60};
danger.text_align = asw::TextJustify::Left;
quit.style = danger;
quit.padding = 12.0F;

Checkbox ​

A button that toggles between checked and unchecked (focusable). A click or activate flips checked and then calls on_change and on_click.

cpp
class Checkbox : public Button;
FieldTypeDescription
checkedboolWhether the box is checked (default: false)
on_changestd::function<void(bool)>Callback invoked with the new value when the user toggles it
texture_checkedasw::TextureTexture when checked. Falls back to texture
texture_checked_hoverasw::TextureTexture when checked and hovered or focused. Falls back to texture_checked
checkbox_stylestd::optional<CheckboxStyle>Style for this checkbox only. Uses the theme checkbox style when empty. The inherited style is not used

It also has every Button field. Without textures, it draws a box with text beside it, styled by CheckboxStyle. With textures, texture is the unchecked image and texture_checked the checked one. Changing checked from code does not call on_change. get_checkbox_style(ctx) returns the style the checkbox draws with.

Choice ​

A button that cycles through a list of options, e.g. difficulty or window mode (focusable). A click or activate moves to the next option, then calls on_click. While focused, left and right move back and forward through the options. When the choice is in a row with other focusable widgets, left and right move focus instead. Each option shows its label from options, its texture from images, or both. The rest of the look comes from the Button style.

cpp
class Choice : public Button;
FieldTypeDefaultDescription
optionsstd::vector<std::string>Option labels
imagesstd::vector<asw::Texture>Option textures, one per option. Optional
indexstd::size_t0Index of the selected option
wrapbooltrueWrap around at either end
adjust_on_left_rightstd::optional<bool>Whether left and right change the option. Empty to decide from the layout (see in_row_with_focusables)
show_arrowsbooltrueShow < and > at the sides while highlighted, when there is text and left and right change the option
on_changestd::function<void(std::size_t)>Callback invoked with the new index when the user changes it
MethodDescription
count()Number of options: the larger of options and images
select(std::size_t i)Select an option without calling on_change. The index is clamped to the options
next() / prev()Move to the next or previous option and call on_change
adjusts_left_right()Whether left and right change the option now
cpp
auto& difficulty = menu.add_child<asw::ui::Choice>();
difficulty.options = {"Easy", "Normal", "Hard"};
difficulty.select(1);
difficulty.on_change = [](std::size_t i) { /* use option i */ };

Slider ​

A horizontal slider for a value between min and max, e.g. volume (focusable). Drag or click the track to set the value. While focused, left and right change the value by step, and up and down move focus. When the slider is in a row with other focusable widgets, left and right move focus instead. Activate the slider to edit it with left and right, then activate or go back to finish.

cpp
class Slider : public Widget;
FieldTypeDefaultDescription
valuefloat0.0Current value
minfloat0.0Smallest value
maxfloat1.0Largest value
stepfloat0.1Change per left or right press
snapboolfalseSnap dragged values to step
on_changestd::function<void(float)>Callback invoked with the new value when the user changes it
adjust_on_left_rightstd::optional<bool>Whether left and right change the value without edit mode. Empty to decide from the layout
stylestd::optional<SliderStyle>Style for this slider only. Uses the theme slider style when empty
MethodDescription
set_value(float v)Set the value without calling on_change. The value is clamped to min and max
is_editing()Whether the slider is in edit mode
adjusts_left_right()Whether left and right change the value now, without edit mode
get_style(const Context& ctx)Get the style the slider draws with
cpp
auto& volume = menu.add_child<asw::ui::Slider>();
volume.value = asw::sound::get_master_volume();
volume.on_change = [](float v) { asw::sound::set_master_volume(v); };

InputBox ​

A text input widget (focusable).

cpp
class InputBox : public Widget;
FieldTypeDescription
on_changestd::function<void(const std::string&)>Callback invoked when the value changes
fontasw::FontFont for the input text. Uses the theme font when empty
valuestd::stringCurrent text value
placeholderstd::stringPlaceholder text shown when empty
stylestd::optional<InputStyle>Style for this input box only. Uses the theme input style when empty

When the text is wider than the box, it scrolls, so the cursor stays in view. Held keys repeat. The box accepts only a left click. Backspace and Delete remove whole UTF-8 characters. You can change value directly; the cursor is moved back into range on the next event. An InputBox cannot be copied. If it is destroyed while it has focus, text input stops. get_style(ctx) returns the style the box draws with.

A box over the rest of the UI that the player must deal with first, e.g. a question or a login code.

cpp
class Modal : public Stack;

Open a modal with Root::open_modal(). While it is open, the screen behind it is dimmed, and the pointer and focus only reach the modal. Its first focusable widget takes focus. When every modal is closed, focus goes back to where it was. The children stack top to bottom and are centered. The modal sizes itself to them, and is at least min_width wide.

FieldTypeDefaultDescription
on_closestd::function<void()>Called one time when the modal closes, from close() or back
close_on_backbooltrueClose when back is pressed. Turn off for a modal that the player must answer with one of its buttons
stylestd::optional<ModalStyle>Style for this modal only. Uses the theme modal style when empty
MethodDescription
add_text(const std::string& text, const asw::Font& font = nullptr)Add a line of text, sized to fit. Returns the Label
add_button(const std::string& text, std::function<void()> on_click)Add a button, sized to its text. Returns the Button
close()Call on_close, then remove the modal. You can call it from the modal's own buttons or from on_close
closed()Returns true after close()
get_style(const Context& ctx)Get the style the modal draws with
cpp
auto& modal = ui.open_modal();
modal.add_text("Quit to the title screen?");
modal.add_button("Quit", [&]() {
  modal.close();
  go_to_title();
});
modal.add_button("Cancel", [&]() { modal.close(); });

Example ​

cpp
#include <asw/asw.h>

asw::core::init(640, 480, 2);

// The root follows the screen size
asw::ui::Root ui;
ui.ctx.theme.font = asw::assets::load_font("font.ttf", 16);
ui.ctx.navigation = asw::ui::bind_default_navigation();

// Customize theme
ui.ctx.theme.button.bg = {60, 60, 80, 255};
ui.ctx.theme.focus_ring.width = 2.0F;

// Build widget tree
auto& layout = ui.root.add_child<asw::ui::VBox>();
layout.transform = {10, 10, 300, 400};

auto& title = layout.add_child<asw::ui::Label>();
title.text = "Settings";
title.transform.size.y = 24.0F;

auto& btn = layout.add_child<asw::ui::Button>();
btn.text = "Start Game";
btn.transform.size.y = 32.0F;
btn.on_click = []() {
  // handle click
};

// A row with a label and a slider
auto& row = layout.add_child<asw::ui::Stack>();
row.direction = asw::ui::Direction::Horizontal;
row.transform.size.y = 20.0F;

auto& volume_label = row.add_child<asw::ui::Label>();
volume_label.text = "Volume";
volume_label.transform.size.x = 100.0F;

auto& volume = row.add_child<asw::ui::Slider>();
volume.transform.size.x = 180.0F;
volume.on_change = [](float v) { asw::sound::set_master_volume(v); };

auto& input = layout.add_child<asw::ui::InputBox>();
input.placeholder = "Enter name...";
input.transform.size.y = 32.0F;
input.on_change = [](const std::string& value) {
  // handle text change
};

// Escape or B when no widget uses it
ui.on_back = []() { asw::core::exit(); };

asw::core::run([&]() {
  asw::core::update();
  ui.update();

  asw::display::clear();
  ui.draw();
  asw::display::present();
});

Released under the MIT License.