1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-06-27 16:36:03 +00:00
luanti/doc/experimental_ui_api.md
2025-06-13 15:09:14 -07:00

86 KiB

UI API

The UI API is the unified replacement for the older formspec and player HUD systems, exposing a new system that is simpler, more robust and powerful, while additionally being less buggy and quirky than its predecessors. It is not yet stable, and feedback is encouraged.

Warning: The UI API is entirely experimental and may only be used for testing purposes, not for stable mods. The API can and will change without warning between versions until it is feature complete and stabilized, including the network protocol.

To use the UI API, Luanti must be compiled with the BUILD_UI CMake option turned on. The UI additionally requires SDL2 support, so USE_SDL2 must also be set. If Luanti is built without BUILD_UI, the ui namespace will still exist in Lua, but the client will not have any C++ UI functionality and all UI network packets will be silently ignored.

This documentation will sometimes refer to features that are not implemented in the current version of the UI API, such as scrollbars and edit fields. These are included in the documentation since they are particularly useful for explaining some of the features of the UI API that can't be explained as precisely otherwise. When this occurs, a block like this precedes the example to note the discrepancy between the documentation and current implementation.

API design

The UI API is exposed to Lua through the global ui namespace of functions and classes. Most of these classes are opaque and effectively immutable, meaning they have no user-visible properties or methods. Users must not access or modify undocumented properties or inherit from any UI class.

All tables passed into UI API functions are defensively copied by the API. Modifying a table after passing it in to a function will not change the constructed object in any way.

Example

Here is a simple example of a working UI made with the UI API:

Since the UI API has no core theme bundled by default at this point (aside from the prelude theme), a few extra style properties are required to make anything visible.

local function builder(context, player, state, param)
    return ui.Window "gui" {
        root = ui.Root {
            size = {108, 76},
            padding = {4},

            box_fill = "black#8C",

            ui.Label {
                pos = {0, 0}, span = {1, 1/2},

                label = "Hello, world!",
            },

            ui.Button "close" {
                pos = {0, 1/2}, span = {1, 1/2},
                box_fill = "maroon",

                label = "Close",

                on_press = function(ev)
                    context:close()
                    minetest.chat_send_player(player, "The window has been closed")
                end,
            },
        },
    }
end

core.register_on_joinplayer(function(player)
    ui.Context(builder, player:get_player_name()):open()
end)

ID strings

Elements require unique IDs, which are represented as strings. ID strings may only contain letters, numbers, dashes, underscores, and colons, and may not be the empty string.

All IDs starting with a dash or underscore are reserved for use by the engine, and should not be used unless otherwise specified. IDs should not use a colon except to include a mod_name: prefix.

Element and group IDs are local to a single window, so the mod_name: prefix used elsewhere in Luanti is generally unnecessary for them. However, if a library mod creates themes, elements, or styles, then using a mod prefix for the library's element and group IDs is highly encouraged to avoid ID conflicts.

Derived element type names are placed in a global namespace, so mods should always use a mod_name: prefix for mod-created derived elements. Only the engine is allowed to make elements with unprefixed type names like switch.

Constructors

The UI API makes heavy use of tables, currying, and Lua's syntactic sugar for function calls to provide a convenient DSL-like syntax for creating UIs. For instance, the curried function signature for elements is ui.Elem(id)(props) or ui.Elem(props), depending on whether the element is given an ID. To illustrate how this is used, here are some examples for creating a label:

-- For elements, the first curried argument is the element ID and the second is
-- a property table defining the element:
ui.Label("my_label")({
    label = "My label text",
})

-- Using Lua's syntactic sugar, we can drop the function call parentheses for
-- string and table literals, which is the preferred convention for the UI API:
ui.Label "my_label" {
    label = "My label text",
}

-- If the ID string or property table is a variable or expression, parentheses
-- are still required around one or both arguments:
local id = "my_label"
ui.Label(id) {
    label = "My label text",
}

-- If the ID is not necessary, it can be omitted altogether:
ui.Label {
    label = "My label text",
}

The constructors for ui.Window and ui.Style use a similar curried function signature.

To further increase the convenience of element and style constructors, certain properties may be "inlined" into the constructor table rather than specified as an explicit table. For example, the list of children for an element can be specified explicitly in the children property, or it can be put directly in the constructor table if the children property is omitted:

-- The `children` property explicitly specifies the list of children.
ui.Group {
    children = {
        ui.Label {label="Child 1"},
        ui.Label {label="Child 2"},
    },
}

-- The `children` property is omitted, so the list of children is taken from
-- constructor table instead.
ui.Group {
    ui.Label {label="Child 1"},
    ui.Label {label="Child 2"},
}

If the children property is explicitly specified, then elements placed directly in the constructor table will be ignored. Other properties that may be inlined follow similar rules.

Unless otherwise documented, it should be assumed that all fields in constructor tables are optional.

Windows

Windows represent discrete self-contained UIs formed by a tree of elements and other parameters that affecting the entire window. Windows are represented by the ui.Window class.

Root element

The window contains a single element, the root element, in which the entire element tree is contained. Because the root element has no parent, it is positioned in the entire screen. The root element must be of type ui.Root.

-- This creates a HUD-type window with the root element as its only element.
ui.Window "hud" {
    root = ui.Root {
        span = {1, 1},
        label = "Example HUD text",
    },
}

Window types

Windows require a window type, which determines whether they can receive user input, what type of scaling to apply to the pixels, and the Z order of how they are drawn in relation to other things on the screen. These are the following:

  • filter: Used for things that need to be drawn before everything else, such as vignettes or filters covering the outside world.
  • mask: Used for visual effects in between the camera and wieldhand, such as masks or other such objects.
  • hud: Used for normal HUD purposes. Hidden when the HUD is hidden.
  • chat: Used to display GUI-like popup boxes or chat messages that can't be interacted with. Hidden when the chat is hidden.
  • gui: Used for GUIs that the user can interact with.

If there are no formspecs open, then the topmost window (that is, the one that was opened last) on the gui layer will receive user input from the keyboard, mouse, and touchscreen. No other layer can receive user input. See the [Events and input] section for more information on how gui windows handle user input.

The gui and chat window types scale all dimensions by real_gui_scaling pixels in size from core.get_player_window_information(), whereas every other window type scales them by real_hud_scaling.

The Z order for window types and other things displayed on the screen is:

  • Luanti world
  • filter window types
  • Wieldhand and crosshairs
  • mask window types
  • Player HUD API elements
  • hud window types
  • Nametags
  • chat window types
  • gui window types
  • Formspecs

If two or more windows of the same type are displayed at the same time, then the windows that were opened more recently will be displayed on top of the less recent ones.

Styling

There are two properties in the window relevant to styling: theme and styles. Both properties use ui.Style objects to select elements from the entire element tree and apply styles to them.

The theme property is meant for using an external theme provided by a game or mod that gives default styling to different elements. If one is not specified explicitly, the current default theme from ui.get_default_theme() will be used instead. See [Theming] for more information.

The styles property is intended for styling that is globally applied to a single window. It takes higher precedence than the theme property, meaning that properties set by any style within styles will override properties set by theme. Any element styling that is specific to this particular window should either reside in the styles property or in local element styles.

ui.Window "gui" {
    -- Set the theme for this window to a theme provided by the mod `my_mod`.
    -- If this line is removed, the default theme set by the game will be used.
    theme = my_mod.get_cool_theme(),

    -- This window also has its own particular styles, such as changing the
    -- text color for some labels. These override properties set by the theme.
    styles = {
        ui.Style "label%warning" {
            text_color = "yellow",
        },
        ui.Style "label%error" {
            text_color = "red",
        },
    },

    root = ui.Root {
        size = {100, 40},

        -- Aside from having any styling from the theme, this label will also
        -- have red text due to the window style. Local styles could also be
        -- added to the element itself to override any of these styles.
        ui.Label {
            groups = {"error"},
            label = "A problem has occurred",
        },
    },
}

ui.Window "gui" {
    theme = my_mod.get_cool_theme(),

    -- If the `styles` property is omitted, global styles can be inlined
    -- directly into the window for convenience.
    ui.Style "label%warning" {
        text_color = "yellow",
    },
    ui.Style "label%error" {
        text_color = "red",
    },

    root = ui.Root {},
}

Elements

Elements are the basic units of interface in the UI API and include such things as sizers, buttons, and edit fields. Elements are represented by the ui.Elem class and its subclasses.

Element IDs

Each element in a window is required to have a unique element ID that is different from every other ID in that window. This ID uniquely identifies the element for both network communication and styling. Elements that have user interaction require an ID to be provided whereas static elements will automatically generate an ID if none is provided.

-- Buttons are an example of an element that require an ID, since they are
-- dynamic and have state on the client.
ui.Button "my_button" {
    label = "My button",
}

-- Labels are fully static, so they don't require an ID.
ui.Label {
    label = "My label",
}

Each element's [Type info] section lists whether IDs must be provided. Elements that are not given an ID will automatically generate one with ui.new_id().

-- Both of these elements will throw an error because buttons need unique IDs
-- that have not been automatically generated.
ui.Button {
    label = "Missing ID",
}

ui.Button(ui.new_id()) {
    label = "Auto-generated ID",
}

If the ID for a dynamic element changes when the UI is updated, this will result in the loss of the element's persistent state, as detailed below.

Styling

Each element has a specific type name that is used when referring to the element in a SelectorSpec. The type name of each element is listed in the element's [Type info] section.

Elements can be styled according to their unique ID. Additionally, elements also have a list of group IDs that allow selectors to style multiple elements at once. Group IDs are only used for styling.

ui.Window "gui" {
    style = ui.Style {
        -- Style all `ui.Button` elements to have a maroon background by
        -- styling the type name `button`.
        ui.Style "button" {
            box_fill = "maroon",
        },

        -- Style all elements with the group `yellow` to have yellow text.
        ui.Style "%yellow" {
            text_color = "yellow",
        },
    },

    root = ui.Root {
        size = {212, 76},

        scale = 1,
        padding = {4},

        box_fill = "black#8C",

        ui.Label {
            pos = {0, 0}, span = {100, 32},

            label = "No style",
        },
        ui.Label {
            pos = {104, 0}, span = {100, 32},

            groups = {"yellow"},
            label = "Yellow text",
        },
        ui.Button "a" {
            pos = {0, 36}, span = {100, 32},

            label = "Maroon background",
        },
        ui.Button "b" {
            pos = {104, 36}, span = {100, 32},

            groups = {"yellow"},
            label = "Yellow text on maroon background",
        },
    },
}

Aside from the global styles found in the window, each element may have local styles of its own that only apply to itself. Effectively, these styles are the same as appending nested styles to the window's list of global styles with a selector that only selects the element's ID; however, a local style is often more convenient. Local styles have higher precedence than styles specified in the window.

-- This button uses local styles to set the button's background color to red
-- and also to make the text yellow when the box is hovered.
ui.Button "local" {
    label = "Hovered yellow",

    styles = {
        ui.Style {
            box_fill = "red",
        },
        ui.Style "$hovered" {
            text_color = "yellow",
        },
    },
}

-- This button inlines the styles into the element itself. Since the first
-- style has no selector, its properties may be inlined into the constructor
-- directly without being wrapped in a `ui.Style` with no selector.
ui.Button "inline" {
    label = "Hovered yellow",

    box_fill = "red",

    ui.Style "$hovered" {
        text_color = "yellow",
    },
}

Child elements

Each element has a list of child elements. Child elements are positioned inside their parent element, and are thus subject to any special positioning rules that a specific element has, such as in the case of scrolled elements.

-- This group element has two buttons as children, positioned side-by-side.
ui.Group {
    ui.Button "left" {
        pos = {0, 0}, span = {1/2, 1},
        label = "Left",
    },
    ui.Button "right" {
        pos = {1/2, 0}, span = {1/2, 1},
        label = "Right",
    },
}

-- Alternatively, we can use the `children` property to include an explicit
-- list of children rather than inlining them into the constructor.
ui.Group {
    children = {
        ui.Button "left" {
            pos = {0, 0}, span = {1/2, 1},
            label = "Left",
        },
        ui.Button "right" {
            pos = {1/2, 0}, span = {1/2, 1},
            label = "Right",
        },
    },
}

-- Both child elements and local styles may be inlined into the constructor
-- table at the same time and mixed freely.
ui.Group {
    ui.Style {
        box_fill = "red",
    },

    ui.Button "fill" {
        label = "Button",
    },
}

The order in which elements are drawn is the parent element first, followed by the first child and its descendants, then the second child, and so on, i.e. drawing takes a pre-order search path.

Each ui.Elem object can only be used once. After being set as the child element of some other element or set as the root of a window, it cannot be reused, either in the same window or in another window.

Persistent fields

Certain elements and the window have properties that can be modified by the user, such as checkboxes or edit fields. However, it must also be possible for the server to modify them. Since there may be substantial latency between client and server, it is undesirable for the server to update every user-modifiable field every time the window is updated, as is the case with formspecs, since that may overwrite the user's input.

These fields that contain user input are called persistent fields. Normal fields use a default value if the server omits the field. Persistent fields, on the other hand, keep their previous value if the server omits the field.

For example, suppose there is a window with a checkbox labelled Check me which the user has checked. Then, the server updates the window, omitting both the label and selected properties. Since label is a normal field, the checkbox's label will become empty. However, selected is a persistent field, so the checkbox will remain checked. If the server had explicitly set the selected property to false, the checkbox would become unchecked.

Changing persistent fields often has side effects. For instance, the UI API doesn't support edit fields yet, but setting the text property on an edit field would cause the caret to move to the end of the text. Similarly, the user may have changed the state of a checkbox before a ui.Context:update() reached the client, so always setting the selected property on that checkbox could overwrite the user's input. As such, it is highly recommended to leave the value for persistent fields at nil unless the server explicitly needs to change the value.

Note that omitted persistent fields are set to a default value when the element is first created, such as when the window is opened or reopened. The selected property, for instance, will be false when the window is first opened unless the server gives it a value.

Derived elements

Often, there are different types of elements that work the same way, but are styled in vastly different ways that make them look like entirely different controls to the user. For instance, ui.Check and ui.Switch are simply toggle buttons like ui.Toggle, but have their own conventional appearances.

Specialized appearances for these different controls can be made by using group IDs, but it is often more convenient to have them act as different element types entirely. Such elements are called derived elements, and can be created using the ui.derive_elem() function.

Derived elements are a purely server-side construct for styling, and act exactly like their normal counterpart on the client. Moreover, all the fields that can be provided to the constructor of the original type can also be provided to the derived type.

As an example, if a mod wanted to create a new special kind of toggle switch, it could create a MyToggle, which acts exactly like a ui.Toggle except for the lack of default styling:

local MyToggle = ui.derive_elem(ui.Toggle, "my_mod:toggle")

local function builder(context, player, state, param)
    return ui.Window "gui" {
        style = ui.Style {
            -- We style our specific toggle with a basic blue color.
            ui.Style "my_mod:toggle" {
                box_fill = "navy",
            },
            ui.Style "my_mod:toggle$selected" {
                box_fill = "blue",
            },

            -- Standard toggles are styled entirely independently of elements
            -- derived from them.
            ui.Style "toggle" {
                box_fill = "olive",
            },
            ui.Style "toggle$selected" {
                box_fill = "yellow",
            },
        },

        root = ui.Root {
            size = {108, 76},
            padding = {4},

            box_fill = "black#8C",

            MyToggle "my" {
                pos = {0, 0}, span = {1, 1/2},
                label = "My toggle",
                selected = true,
            },
            ui.Toggle "ui" {
                pos = {0, 1/2}, span = {1, 1/2},
                label = "Standard toggle",
            },
        },
    }
end

core.register_on_joinplayer(function(player)
    ui.Context(builder, player:get_player_name()):open()
end)

All standard derived elements can be found in the [Derived elements] section of their respective element's documentation.

Boxes

Elements handle state and behavior, but the elements themselves are invisible and can't be styled on their own. Instead, elements contain boxes, which are rectangular regions inside each element that can be styled and denote the visible bounds of each part of the element.

Boxes can be styled with any of the style properties listed in StyleSpec, such as box images or padding. Boxes also contain certain types of state information relevant to styling, such as whether the mouse was pressed down within the box's boundaries.

Box positioning

Much like elements, boxes are arranged in a tree where each box can have one or more child boxes. Every element has a main box which serves as the ancestor of every other box in the element. The only exception to this rule is ui.Root, which has a backdrop box as the parent of the main box.

Each element type has a predefined set of boxes in a fixed hierarchy. For instance, the box hierarchy for scrollbars looks like the following:

Note

serves as a representative example of box hierarchies.

+-----+--------------+-------------+-------------------------------+-----+
| /__ |              |      =      |                               | __\ |
| \   |              |      =      |                               |   / |
+-----+--------------+-------------+-------------------------------+-----+
^^^^^^^.            .^^^^^^^^^^^^^^^.                             .^^^^^^^
decrease            .     thumb     .                             increase
.      .            .               .                             .      .
.      ^^^^^^^^^^^^^^               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^      .
.      .   before                                after            .      .
.      .                                                          .      .
.      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^      .
.                                  track                                 .
.                                                                        .
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                                   main

Specifically, the main box has the scrollbar track box and the decrease and increase boxes as children. The track box, in turn, has the thumb box as a child to indicate the value of the scrollbar, plus before and after boxes on either side of the thumb that can be used to give a separate style to each half of the scrolled region.

Boxes can have special positioning rules for their children. For scrollbars, the decrease, increase, and track boxes have a bounding rectangle that spans the entire main box. As such, they can be positioned freely, e.g. the buttons could be moved to the left side of the scrollbar or even hidden altogether. The bounding rectangle for the thumb box, however, depends on the value of the scrollbar since the thumb moves as the value changes. Therefore, thumb can only be positioned in a limited fashion within its bounding rectangle. The situation is similar for before and after.

Boxes are also in charge of where the content of the element is placed. For simple elements like ui.Button, the children of the element and the label are both positioned within the main box. More complex elements often handle things differently. For instance, the ui.Caption element uses separate boxes for these, caption for the label and content for the child elements.

The list of boxes for each element is described in the element's [Type info] section. The children and content of each box, plus any special behavior they might have, is documented there.

Input and interaction

Elements do not react to mouse or touch input directly. Instead, boxes within the element handle these types of input, reacting to various situations such as when the box is pressed or the cursor is hovering over the box.

In the scrollbar example above, the decrease and increase boxes act like buttons that can change the value of the scrollbar. The thumb is a box that can be dragged by the pointer within the bounds of the track. These are known as dynamic boxes since they respond to mouse and touch input. On the other hand, the main and track boxes are inert and ignore mouse and touch input altogether. These are called static boxes.

Static boxes are invisible to mouse or touch events and let them pass through to the box directly underneath them, and hence styling the hovered or pressed states never has any effect. Dynamic boxes, on the other hand, respond to events, so they always respond to styling the pressed, hovered, and focused states. Moreover, dynamic boxes generally do not let mouse or touch events pass through them. In particular, only the topmost dynamic box under the cursor will have the hovered state set.

To illustrate how events pass through different boxes, consider a button with two children, a checkbox and a static label. If the mouse hovers over the checkbox, which has a dynamic main box, the checkbox will have the hovered state whereas the parent button will not. However, if the mouse hovers over the label, which has a static main box, the event will pass through the label and the parent button will have the hovered state instead.

Events and input

Events are the means through which user input is communicated from the client to the server. They range from detecting when a checkbox was pressed to notifying when focus changed from one element to another.

Events come in two variants: window events are events that are global to the entire window, such as when the window has been closed or focus has changed. Element events, on the other hand, fire when an event happens to a specific element, such as a scrollbar being moved or a checkbox being toggled.

When a server receives an event from the client, it calls the appropriate event handler. Event handlers are callback functions that can be provided to the window or to elements as properties. The signature of every event handler is function(ev), where ev is an EventSpec containing information about the event, such as the new value of the scrollbar that was changed. See the EventSpec documentation for the generic fields supported by all events.

Network latency

Beware of the effects of network latency on event handlers, since an event coming from the previous state of the element may surface after the element has been updated by the server. For instance, the user might click a button, and the server disables a checkbox in response. However, the user clicked the checkbox before the client received the server's update, causing the server to receive a checkbox event after it disabled the checkbox.

The server could filter out these events, e.g. dropping all events for disabled checkboxes, but this might lead to inconsistent state between the client and server. In the previous example, the client would see the checkbox as checked, but the server would still believe the checkbox was unchecked. This is a bad situation, so the server will never drop events outright (except if the event was for an element that was since removed). However, it will ensure that the data contained in the event is valid, e.g. by clamping a scrollbar's value to the current range of the scrollbar.

Additionally, note that fake events may come from malicious clients, such as a button press for a button that was never enabled. The server will filter out events that are obviously incorrect, such as when they come from the wrong player or from a window that is no longer open. Validation of the data contained within each event is another line of defense. However, any stronger validation, such as checking whether the user could have clicked that button at all (given that it was always disabled) is impractical for the UI API to check automatically, and hence the responsibility lies on the event handler if absolute security is necessary.

Input dispatching

Since windows usually contain many elements that may be nested or overlapping, user input is dispatched to elements in a specific order. This also has important impacts on if and when event handlers are called.

First, the window contains a focused element, which is the element that has keyboard focus. Elements that contain dynamic boxes, such as buttons and scrollbars, can be focused. Static elements can never be focused.

When a keyboard key is pressed or released, the window allows the focused element to use the event first. If it ignores the input (such as if the element has no dynamic boxes), the window allows its parent to use the input, and so forth. If none of them used the keyboard input, or if there is no focused element, then the window itself gets to use the input, possibly sending it to the server.

The window also contains a hovered element, which is the element that primarily receives mouse and touch input. Just like the focused element, only dynamic boxes may be the hovered element.

When mouse or touch input is received by the window, it first allows the focused element and its parents to peek at the event, such as to let buttons become unpressed. Then, it sends the input to the hovered element. If it ignores the input, the input passes to the element directly underneath it at the mouse/touch position (which is not necessarily its parent element), and so on. If none of them used the input, the window again gets to use the input, possibly sending it to the server.

Window events

The window has a few predefined ways of using user input besides passing the input along to the server.

The mouse is used for a number of features. When the mouse moves, the window updates the currently hovered element. Similarly, when the left mouse button is pressed, the window sets the focused element to the topmost focusable element under the cursor.

If certain keys are not handled by the focused element, then the window uses a few of them for special purposes. The Escape key will cause the window to be closed if allow_close is not set to false for the window, which will naturally cause the on_close event to be fired. The Enter key will cause the on_submit event to be sent to the server, which the server may use for whatever purpose it desires, such as by closing the window and using the form's data somewhere.

The Tab key is used for changing which element has user focus via the keyboard. Pressing Tab will cause the next focusable element in a preorder traversal of the element tree to become focused, whereas pressing Shift + Tab will transfer focus to the previous focusable element.

Styles

Styles are the interface through which the display and layout characteristics of boxes are changed. They offer a wide variety of style properties that are supported by every box. Styles can vary based on states, which indicate how the user is interacting with a box. Styles are represented with the ui.Style class.

There are three components of a style:

  1. A SelectorSpec that decides which boxes and states to apply the style to.
  2. A StyleSpec that contains the actual styling properties.
  3. A list of nested styles with their own selectors and properties that are cascaded with the ones in the parent style.

One simple example of a style is the following:

-- Select all buttons with the group ID `important` and give them yellow text
-- and a blue background.
ui.Style "button%important" {
    text_color = "yellow",
    box_fill = "blue",
}

-- Alternatively, a `StyleSpec` with style properties can be included
-- explicitly with the `props` property rather than being inlined:
ui.Style "button%important" {
    props = {
        text_color = "yellow",
        box_fill = "blue",
    },
}

Selectors and properties

See the respective sections on SelectorSpec and StyleSpec for advanced details on selectors and the list of supported style properties. Additionally, a comprehensive description of how style properties affect the layout and display of boxes is also discussed in the [Layout and visuals] section.

States

The list of states is as follows, from highest precedence to lowest:

  • disabled: The box is disabled, which means that user interaction with it does nothing.
  • pressed: The left mouse button was pressed down inside the boundaries of the box, and has not yet been released.
  • hovered: The mouse cursor is currently inside the boundaries of the box.
  • selected: The box is currently selected in some way, e.g. a checkbox is checked or a list item is selected.
  • focused: The box currently has keyboard focus.

The pressed and hovered states are properties of the box itself, and hence only apply to dynamic boxes. Any dynamic box can be hovered and pressed, although different boxes may have different requirements for what constitutes being pressed.

The disabled, selected, and focused states are often shared among multiple boxes, and hence may apply to both static and dynamic boxes. For example, when an individual scrollbar is disabled, every box within that scrollbar has the disabled state.

By default, it should be assumed that the focused state applies to every box within the element if the element has focus. Moreover, if the element has a disabled property, then the disabled state will also apply to every box when that property is set. Boxes that have different behavior document how they behave instead.

State cascading

States fully cascade over each other. For instance, if there are styles that give hovered buttons yellow text and pressed buttons blue backgrounds, then a hovered and pressed button will have yellow text on a blue background.

-- Creating two styles with separate states like so...
ui.Style "button$hovered" {
    text_color = "yellow",
}
ui.Style "button$pressed" {
    box_fill = "blue",
}

-- ...automatically implies the following cascaded style as well:
ui.Style "button$hovered$pressed" {
    text_color = "yellow",
    box_fill = "blue",
}

However, if an element is currently in multiple states, then states with higher precedence will override states with lower precedences. For instance, if one style makes hovered buttons red and another makes pressed buttons blue, then a button that is simultaneously hovered and pressed will be blue.

-- One style makes hovered buttons red, and another makes pressed buttons blue.
ui.Style "button$hovered" {
    box_fill = "red",
}
ui.Style "button$pressed" {
    box_fill = "blue",
}

-- Then, the implicit cascaded style will make buttons blue:
ui.Style "button$hovered$pressed" {
    box_fill = "blue",
}

Lastly, state precedences can combine to form a higher precedence state. A style for pressed and hovered buttons will override styles for only pressed or only hovered buttons. Similarly, a style for disabled buttons will override the style for pressed and hovered buttons since it has higher precedence than either one.

-- The first style makes hovered buttons red.
ui.Style "button$hovered" {
    box_fill = "red",
}

-- The second style makes buttons that are both hovered and pressed blue.
-- Therefore, hovered buttons will only be red if they are not pressed.
ui.Style "button$hovered$pressed" {
    box_fill = "blue",
}

-- If a button is disabled, this style makes it gray regardless of whether it
-- is hovered or pressed.
ui.Style "button$disabled" {
    box_fill = "gray",
}

Nested styles

Nested styles are a way of adding styles to a base style that apply extra properties to a subset of the boxes. For instance, here is an example that shows how nested styles work:

-- Here is a style with nested styles contained within it.
ui.Style "button" {
    box_fill = "yellow",

    ui.Style "$hovered" {
        box_fill = "red",
    },
    ui.Style "$pressed" {
        box_fill = "blue",
    },
}

-- That style is equivalent to having the following three styles:
ui.Style "button" {
    box_fill = "gray",
}
ui.Style "button$hovered" {
    box_fill = "red",
}
ui.Style "button$pressed" {
    box_fill = "blue",
}

-- The nested styles can also be explicitly included with the `nested` property
-- rather than being inlined into the style table:
ui.Style "button" {
    box_fill = "yellow",

    nested = {
        ui.Style "$hovered" {
            box_fill = "red",
        },
        ui.Style "$pressed" {
            box_fill = "blue",
        },
    },
}

Nested styles are always evaluated top to bottom, so the parent style is applied to the box first, and then each nested style is applied to the box in order. This order-dependent styling is in direct contrast to CSS, which calculates precedence based on weights associated with each part of the selector. Order-dependence was deliberately chosen because it gives mods more control over the style of their own windows without external themes causing problems. The only weighting done by the UI API is for state selectors, as described above, due to states being calculated on the client.

There is no reason why a parent style containing nested styles must have a selector or properties; nested styles can just be used for organizational purposes by placing related styles in an empty parent style. Omitting the selector causes it to be automatically set to the universal selector *. For example, this style might be used as a theme for ui.set_default_theme():

local theme = ui.Style {
    ui.Style "button, toggle, option" {
        -- Style properties for all the different types of buttons.
    },
    ui.Style "check, switch" {
        -- Style properties for checkboxes and switches.
    },
    ui.Style "radio" {
        -- Style properties for radio buttons.
    },
}

Style resets

Sometimes, it is desireable to remove all existing style from a certain element type. This can be done via style resets. The reset boolean, when set, resets all style properties for the selected elements to their default values.

For instance, if one style gives buttons a red background and nonzero padding, then setting the reset property on a later style for buttons will reset that to the defaults of a transparent background and no padding. Note that this will also reset any properties set by the prelude theme, described in [Theming].

-- One style somewhere adds a bunch of properties to buttons.
ui.Style "button" {
    box_fill = "red",
    padding = {2, 2, 2, 2},
}

-- The button with the ID "special" needs unique styling, and hence uses the
-- `reset` property to get a clean slate for styling.
ui.Style "#special" {
    reset = true,
    text_color = "yellow",
}

Style resets cascade with style states as well. Resetting buttons with the hovered state will also reset the properties for buttons that are both hovered and pressed, for instance. Resetting button directly will reset every state for the button as well.

Theming

It is usually the case that different windows share the same basic element styles. This concept is supported natively by the UI API through the use of themes, which are large groups of styles that can be used in the theme property of windows or set as the globally default theme.

If a window doesn't have the theme property set, it will automatically use the default theme, which is retrieved by ui.get_default_theme(). The engine initially sets the default theme to the prelude theme, but games and mods can change the default theme with the ui.set_default_theme() function. In the future, the UI API will include a default theme called the "core theme", which games can optionally use if they don't want to make their own theme or are in early stages of development.

Prelude theme

Nearly all themes should be based on the prelude theme, a theme that serves no other purpose than being a base for other themes. The prelude theme sets style properties that are essential to the functionality of the element, or are at least a basic part of the element's intended design. It can be accessed with the ui.get_prelude_theme() function.

For instance, it is part of the basic functionality of ui.Accordion to hide and show its contents, so the prelude hides the content box if the selected state is not active. As another example, ui.Image almost always wants its image to fill as much of the element as possible, so the prelude sets their icon_scale property to zero.

It is important to stress that the prelude theme does not change the default visuals of any elements. Box and icon images, tints, fills, paddings, margins, and so on are never changed by the prelude theme, meaning all elements are totally invisible by default and free to be themed.

The prelude theme is designed to be highly stable, and should rarely change in any substantial way. Moreover, each element documents what default styling the prelude theme gives it in its [Theming] section, making it easy for new themes to override prelude styling where necessary.

When creating a new theme, the prelude theme should be included like so:

local new_theme = ui.Style {
    -- Include the prelude theme into this theme.
    ui.get_prelude_theme(),

    -- Add any number of new styles to the theme, possibly overriding
    -- properties set by the prelude theme.
    ui.Style "root" {
        box_fill = "navy#8C",
    },
}

To make a window with no theme, it is recommended to set the theme property to ui.get_prelude_theme(). On rare occasions, it may be useful to make a window that contains no styles, including those set by the prelude. In that case, the theme property can be set to a blank ui.Style {}.

Layout and visuals

The UI API has powerful support for positioning, sizing, and styling the visuals and display of boxes. Rather than just supporting the strange coordinate system of formspecs or the normalized coordinates of the player HUD API, the UI API supports both pixel positioning and normalized coordinates. Future versions will also support more advanced flex and grid layouts. Moreover, every box supports a universal set of style properties for visuals, rather than the limited and element-dependent styling of formspecs.

See StyleSpec for a full list of supported style properties.

Box layout

Boxes have a number of paddings and margins can be applied to add space inside and around themselves. This leads to a number of conceptual rectangular areas inside each box that serve various purposes. The following diagram illustrates each of these rectangles:

+---------------------------------------------------------+
|  Layout rect                                            |
|   +-------------------------------------------------+   |
|   |* Display rect  * * * * * * * * * * * * * * * * *|<->|
|   | * +-----------------------------------------+ * | Margin
|   |* *|  Middle rect                            |* *|   |
|   | * |                +--------------------+   |<->|   |
|   |* *|   +--------+   |  Content rect      |   | Middle rect
|   | * |   |* Icon *|   |      |   |  .      |   | * border
|   |* *|   | * rect |<->|      |---|  |      |<->|* *|   |
|   | * |   |* * * * | Gutter   |   |  |      | Padding   |
|   |* *|   +--------+   |                    |   |* *|   |
|   | * |                +--------------------+   | * |   |
|   |* *|                                         |* *|   |
|   | * +-----------------------------------------+ * |   |
|   |* * * * * * * * * * * * * * * * * * * * * * * * *|   |
|   +-------------------------------------------------+   |
|                                                         |
+---------------------------------------------------------+

After the box is positioned within its bounding rectangle (as described below), the resulting positioned rectangle is called the layout rectangle. The contents of the layout rectangle are inset by the margin property, making it possible to add space between adjacent boxes.

Inside the layout rectangle is the display rectangle, which as the name implies is where the box is displayed. This is where the box_* style properties draw their contents, e.g. box_image draws an image that is stretched to the boundaries of the bounding rectangle. Additionally, the display rectangle is where mouse and touch input are detected.

If the box_middle property is set, then the box_image is drawn as a nine-slice image where the image borders are scaled by the box_scale property. The contents of the display rectangle are automatically inset by the size of this border. This results in the middle rectangle, which in turn insets its contents by the padding property to make the padding rectangle.

Placed inside the padding rectangle is the content rectangle, which is where the content of the box is placed. This might include text, child boxes, and/or other special types of content contained in the element, or it might contain nothing at all, such as in the case of scrollbar buttons and thumbs.

Finally, if the icon_image property is set, then an icon rectangle is allocated to make space for the icon. The size of this rectangle is based on the size of the icon image scaled by the icon_scale property. By default, the icon rectangle is placed in the center of the content rectangle, but it can be moved to any side of the content by using the icon_place property. The icon_overlap property controls whether the content rectangle should overlap the icon rectangle, which will cause any content to be displayed on top of the icon. If they are not set to overlap, the icon_gutter property can be used to control how much space will be placed between them.

Content layout

TODO

Place layout

Flex and grid layout

Visual properties

Some style properties that control the visual aspects of boxes have been described above. However, there are other properties that can modify the appearance of the box.

The display property controls whether the box and its contents are visible and/or clipped without affecting the layout of the box. The default is visible, which makes the box and its contents visible, but they will be clipped to the bounding rectangle. To prevent the box from being clipped at all, the overflow value can be used instead. If the hidden value is used, then the content will be displayed as normal, but the box and its icon will not be drawn, even though they are still present and can be clicked as normal. Lastly, the clipped value can be used to clip the box and its contents away entirely. The box and contents still exist and take up space, but the mouse will be unable to interact with them (although the keyboard still can). Descendants of a clipped box can be shown by using the overflow value.

The box_* and icon_* properties have some overlap in terms of style properties. The *_fill properties fill the respective rectangle with a solid color. As mentioned before, the *_image properties choose the image to draw inside the rectangle. To draw only part of the image, a source rectangle can be specified in normalized coordinates with *_source. The image can also be colorized by using *_tint. Finally, for animated images, *_frames can be used to specify the number of frames in the image, along with *_frame_time to set the length of each frame in milliseconds.

There are also some properties unique to both. As mentioned, the icon can use icon_scale to set a scale factor for the icon. If this scale is set to 0, then the icon rectangle fills up as much of the box as possible without changing the aspect ratio of the image.

For the display rectangle, the aforementioned box_middle property sets the image to be a nine-slice image. Like box_source, it is specified in normalized coordinates based on the image size. The box_tile property can be used to have the image be tiled rather than streched, which tiles each slice individually for a nine-slice image. Finally, the box_scale property scales the size of each tile in a tiled image and the borders of a nine-slice image.

Contexts

In order to show a window, the UI API provides contexts, represented by the ui.Context class. Contexts encapsulate the state of a single window and the player it is shown to, and provide a means of opening, closing, rebuilding, and updating the window. Unlike most classes in the UI API, ui.Context is not immutable and contains public methods that can modify its state.

Builder functions

In order to create a context, a builder function must be provided. When called, this function should return a window containing the desired element tree. Every time the window is shown to a player or updated, the builder function is called again. This allows the function to change the number of elements or the properties they have. Since window and element objects are immutable to the user, rebuilding everything using the builder function is the only way to modify the UI.

Each ui.Window object can only be used once. After being returned from a builder function once, the same object cannot be returned from a builder function again. The same applies to elements inside the window.

Besides the builder function, the ui.Context constructor requires the name of the player that the window is shown to. The builder function and player can be extracted from a context using the get_builder() and get_player() methods respectively.

Opening, updating, and closing windows

After a context has been constructed, it can be shown to the player using the open() method. If the state has changed and the UI needs to be rebuilt and shown to the player, the update() method can be called. To close the window programmatically, use the close() method. Open windows will be automatically closed when the player leaves the game.

When a window is closed, the update() and close() methods do nothing. Similarly, when the window is open, the open() method does nothing. If multiple windows need to be shown to a player, then multiple contexts must be created since a context represents a single window.

To query whether a context is open, use the is_open() method. To get a list of all currently open contexts, use the ui.get_open_contexts() function.

Non-updatable properties

Some properties cannot be updated via the update() method since that could lead to race conditions where the server changed a property, but the client sent an event that relied on the old property before it received the server's changes. For most situations, this is not problematic, but it could cause weird behavior for some changes, such as when changing the window type from gui to another window type that doesn't use events.

To change these non-updatable properties, use the reopen() method, which is effectively the same as calling context:close():open(), but will do so in a single atomic step (i.e. the player won't see the window disappear and reappear). Additionally, reopen() will do nothing if the window is already closed, just like update().

The reopen() method is not an exact replacement for update(). For one, it will change the window's Z order by moving it in front of all other windows with the same window type. Additionally, all persistent properties will be changed by this operation, just like open().

State and parameter tables

Since UIs generally have some state associated with each open window, contexts provide a means of holding state across calls to the builder function via a state table. When a ui.Context is constructed, a state table can be provided that holds state that will be passed to the builder function every time it is called for this UI. This table can be modified by the builder function or by event handlers in the UI. The UI API will never modify a state table by itself. The state table can be obtained from a context via the get_state() method or replaced entirely via set_state().

The following is a basic example that uses a state table:

local function builder(context, player, state, param)
    return ui.Window "gui" {
        root = ui.Root {
            size = {108, 40},
            padding = {4},

            box_fill = "black#8C",

            ui.Button "counter" {
                box_fill = "maroon",
                label = "Clicks: " .. state.num_clicks,

                on_press = function(ev)
                    state.num_clicks = state.num_clicks + 1
                    context:update()
                end,
            },
        },
    }
end

core.register_on_joinplayer(function(player)
    local state = {num_clicks = 0}
    ui.Context(builder, player:get_player_name(), state):open()
end)

The state table is primarily for persistent data. However, it is often useful to send temporary data that only applies to a single call of the builder function, such as when setting persistent fields. This can be provided in the form of a parameter table. Parameter tables can be provided to the methods open(), update(), and reopen() and will be passed directly to the builder function. After the builder function returns, the parameter table is discarded.

The following is a basic example that sets a persistent field when first opening the window by using parameter tables (note that it uses a slider, which is not implemented yet):

local function builder(context, player, state, param)
    return ui.Window "gui" {
        root = ui.Root {
            size = {500, 40},
            padding = {4},

            box_fill = "black#8C",

            ui.Slider "percent" {
                label = state.scroll .. "%",

                min = 0,
                max = 100,

                -- `param.set_scroll` is only set in `open()`, not in the
                -- `update()` in `on_scroll`, so `value` is only set to
                -- `state.scroll` when the window is first opened.
                value = param.set_scroll and state.scroll,

                on_scroll = function(ev)
                    state.scroll = ev.value
                    context:update()
                end,
            },
        },
    }
end

core.register_on_joinplayer(function(player)
    local state = {scroll = 50}
    local param = {set_scroll = true}

    ui.Context(builder, player:get_player_name(), state):open(param)
end)

Utility functions

  • ui.new_id(): Returns a new unique ID string.
    • This function may not be used for elements that require an ID to be provided, such as buttons or edit field elements! These elements require an ID that will stay constant across window updates!
    • It is usually not necessary to use this function directly since the API will automatically generate IDs when none is required.
    • The format of this ID is not precisely specified, but it will have the format of an engine reserved ID and will not conflict with any other IDs generated during this session.
  • ui.is_id(str): Checks whether the argument is a string that follows the format of an ID string.
  • ui.get_coord_size(): Returns the size of a single coordinate in a fixed-size formspec, i.e. a formspec with a size of size[<x>,<y>,true]. Can be used when transitioning from formspecs to the UI API.
  • ui.derive_elem(elem, name): Creates and returns a new derived element type.
    • elem: The element class to derive from, e.g. ui.Toggle.
    • name: The type name for the derived element to use. The name should use a mod_name: prefix.
    • Returns the constructor for the new type, which can be used to create new elements of the new derived type.
  • ui.get_prelude_theme(): Returns the style defining the prelude theme.
  • ui.get_default_theme(): Returns the style used as the default theme for windows without an explicit theme. Defaults to the prelude theme for now.
  • ui.set_default_theme(theme): Sets the default theme to a new style.
  • ui.get_open_contexts(): Returns a table containing the context objects for all currently open windows.

ui.Context

Contexts encapsulate the state of a single window shown to a specific player, as described in the [Contexts] section.

Constructor

  • ui.Context(builder, player[, state]): Creates a new context with a player and an initial state table. The window is initially not open.
    • builder (function): The builder function for the context. This function takes four parameters, function(context, player, state, param):
      • context (ui.Context): The context itself.
      • player (string): The name of the player that the window will be shown to, equivalent to context:get_player().
      • state (table): The state table associated with this context, equivalent to context:get_state().
      • param (table): The parameter table for this call to the builder function.
      • The function should return a freshly created ui.Window object.
    • player (string): The player the context is associated with.
    • state (table, optional): The initial state table for the window. If not provided, defaults to an empty table.

Methods

  • open([param]): Builds a window and shows it to the player. Does nothing if the window is already open.
    • param (table, optional): The parameter table for this call to the builder function. If not provided, defaults to an empty table.
    • Returns self for method chaining.
  • update([param]): Updates a window by rebuilding it and propagating the changes to the player. Does nothing if the window is not open.
    • param (table, optional): The parameter table for this call to the builder function. If not provided, defaults to an empty table.
    • Returns self for method chaining.
  • reopen([param]): Reopens a window by rebuilding it, closing the player's old window, and showing the new window to the player atomically. Does nothing if the window is not open.
    • param (table, optional): The parameter table for this call to the builder function. If not provided, defaults to an empty table.
    • Returns self for method chaining.
  • close(): Closes a window that is currently shown to the player. Does nothing if the window is not currently open.
    • Returns self for method chaining.
  • is_open(): Returns true if the window is currently open, otherwise false.
  • get_builder(): Returns the builder function associated with the context.
  • get_player(): Returns the player associated with the context.
  • get_state(): Returns the state table associated with the context.
  • set_state(state): Sets a new state table for the context, replacing the existing one.
    • Returns self for method chaining.

ui.Window

Windows represent discrete self-contained UIs as described in the [Windows] section.

Constructor

  • ui.Window(type)(props): Creates a new window object.
    • type (string): The window type for this window. This field cannot be changed by ui.Context:update().
    • props (table): A table containing various fields for configuring the window. See the [Fields] section for a list of all accepted fields.

Fields

The following fields can be provided to the ui.Window constructor:

  • root (ui.Root, required): The root element for the element tree.
  • theme (ui.Style): Specifies a style to use as the window's theme. Defaults to the theme provided by ui.get_default_theme().
  • styles (table of ui.Styles): A table of global styles that apply across the entire element tree. If this property is omitted, global styles may be inlined into the constructor table.
  • allow_close (boolean): Indicates whether the user is able to close the window via the Escape key or similar. Defaults to true. This field cannot be changed by ui.Context:update().

The following persistent fields can also be provided:

  • focused (ID string): If present, specifies the ID of an element to set as the focused element. If set to the empty string, no element will have focus. Newly created windows default to having no focused element.

The following event handlers can also be provided:

  • on_close: Fired if the window was closed by the user. This event will never be fired for windows with allow_close set to false. Additionally, the player leaving the game and ui.Context:close() will never fire this event.
  • on_submit: Fired if the user pressed the Enter key and that keypress was not used by the focused element.
  • on_focus_change: Fired when the user changed the currently focused element. Additional EventSpec fields:
    • unfocused: Contains the ID of the element that just lost focus, or the empty string if no element was previously focused.
    • focused: Contains the ID of the element that just gained focus, or the empty string if the user unfocused the current element.

ui.Elem

Elements are the basic units of interface in the UI API, as described in the [Elements] section. All elements inherit from the ui.Elem class.

In general, plain ui.Elems should not be used when there is a more appropriate derived element, such as ui.Image for an element that has the sole purpose of displaying an image. Moreover, plain ui.Elems should never be given default styling by any theme.

Type info

  • Type name: elem
  • ID required: No
  • Boxes:
    • main (static): The main box of the element. Unless otherwise stated in the documentation for other boxes, text content and child elements are placed within this box.

Derived elements

  • ui.Group
    • Type name: group
    • A static element meant for holding generic grouping of elements for layout purposes, similar to the HTML <div> element.
  • ui.Label
    • Type name: label
    • A static element that is meant to be used for static textual labels in a window. The regular label property should be used to display the text for the label.
  • ui.Image
    • Type name: image
    • A static element that only meant for displaying an image, either static or animated. The icon_image style property should be used to display the image, not the box_image property.

Theming

For ui.Image, the prelude sets icon_image to zero to make the image fill as much space as possible. There is no prelude theming for either ui.Elem or ui.Label.

Constructor

All elements have the same function signatures as ui.Elem for their constructors and only differ in the properties accepted for props.

  • ui.Elem(id)(props): Creates a new element object.
    • id (ID string): The unique ID for this element. It is only required if stated as such in the element's [Type info] section.
    • props (table): A table containing various fields for configuring the element. See the [Fields] section for a list of all accepted fields.
  • ui.Elem(props): Same as the above constructor, but generates an ID with ui.new_id() automatically.

Fields

The following fields can be provided to the ui.Elem constructor:

  • label (string): The text label to display for the element.
  • groups (table of ID strings): The list of group IDs for this element.
  • props (StyleSpec): A table of style properties that only apply to this element's main box without any state selectors applied. If this property is omitted, these style properties may be inlined into the constructor table.
  • styles (table of ui.Styles): A table of local styles that only apply to this element. If this property is omitted, local styles may be inlined into the constructor table.
  • children (table of ui.Elems): The list of elements that are children of this element. If this property is omitted, children may be inlined into the constructor table.

ui.Root

The root element is a special type of element that is used for the sole purpose of being the root of the element tree. Root elements may not be used anywhere else in the element tree.

Aside from its main box, the root element also has a backdrop box as the the parent of its main box. The backdrop takes up the entire screen behind the window and is intended to be used for dimming or hiding things behind a window with a translucent or opaque background.

The root element is fully static, but the backdrop box will have the focused state set if the window is a GUI window that has user focus. In general, the backdrop box should be invisible unless it is focused. This prevents backdrops from different windows from stacking on top of each other, which generally leads to an undesirable appearance.

The backdrop box does not count as part of the element for mouse clicks, so clicking on the backdrop counts as clicking outside the window.

Type info

  • Type name: root
  • ID required: No
  • Boxes:
    • backdrop (static): The backdrop box, which has the entire screen as its layout rectangle. It is the parent of the main box.
    • main (static): The main box. See ui.Elem for more details.

Theming

The prelude centers the main box on the screen with pos and anchor, but gives it no size, so users must explicitly choose a size with span and/or size. Additionally, the prelude sets display to hidden for the backdrop unless it has the focused state set. The backdrop also sets clip to both to ensure that the backdrop never expands past the screen size even if its content does.

Fields

The fields that can be provided to the ui.Root constructor are the same as those in ui.Elem.

ui.Button

The button is a very simple interactive element that can do nothing except be clicked. When the button is clicked with the mouse or pressed with the spacebar, then the on_press event is fired unless the button is disabled.

Type info

  • Type name: button
  • ID required: Yes
  • Boxes:
    • main (dynamic): The main box, which constitutes the pressable portion of the button. Also see ui.Elem for more details.

Theming

There is no prelude theming for ui.Button.

Fields

In additional to all fields in ui.Elem, the following fields can be provided to the ui.Button constructor:

  • disabled (boolean): Indicates whether the button is disabled, meaning the user cannot interact with it. Default false.

The following event handlers can also be provided:

  • on_press: Fired if the button was just pressed.

ui.Toggle

The toggle button is a type of button that has two states: selected and deselected. In its simplest form, the state of the toggle button flips between selected and deselected whenever it is pressed. The state of the toggle button can be controlled programmatically by the persistent selected property.

Toggle buttons have two events, on_press and on_change, which fire simultaneously. Although on_change is a strict superset of the functionality of on_press, both are provided for parity with ui.Option.

Type info

  • Type name: toggle
  • ID required: Yes
  • Boxes:
    • main (dynamic): The main box, which constitutes the pressable portion of the toggle button. The selected state is active when the toggle button is selected. Also see ui.Elem for more details.

Derived elements

  • ui.Check
    • Type name: check
    • A variant of the toggle button that is meant to be styled like a traditional checkbox rather than a pushable button.
  • ui.Switch
    • Type name: switch
    • A variant of the toggle button that is meant to be styled like a switch control; that is, a horizontal switch where left corresponds to deselected and right corresponds to selected.

Theming

The prelude horizontally aligns the icon image for ui.Check and ui.Switch to the left. There is no prelude theming for ui.Toggle.

Fields

In additional to all fields in ui.Elem, the following fields can be provided to the ui.Toggle constructor:

  • disabled (boolean): Indicates whether the toggle button is disabled, meaning the user cannot interact with it. Default false.

The following persistent fields can also be provided:

  • selected (boolean): If present, changes the state of the toggle button to a new value. Newly created toggle buttons default to false.

The following event handlers can also be provided:

  • on_press: Fired if the toggle button was just pressed.
  • on_change: Fired if the value of the toggle button changed. Additional EventSpec fields:
    • selected (boolean): The state of the toggle button.

ui.Option

The option button is similar to a toggle button in the sense that it can be either selected or deselected. However, option buttons are grouped into families such that exactly one option button in each family can be selected at a time by the user.

The family of an option button is controlled by the family property, which is set to a ID string that is shared by each option button in the family. If no family is provided, the option button acts as if it were alone in a family with one member.

When the user presses a non-disabled option button, that button is selected and all the other buttons in the family (including disabled ones) are deselected. Although the user can only select one option button, zero or more option buttons may be set programmatically via the persistent selected property.

Option buttons have two events: on_press and on_change. The on_press event occurs whenever a user presses an option button, even if that button was already selected. The on_change event occurs whenever the value of an option button changes, whether that be the button the user selected or the other buttons in the family that were deselected. The on_change event will fire for the selected button first, followed by the deselected buttons.

Type info

  • Type name: option
  • ID required: Yes
  • Boxes:
    • main (dynamic): The main box, which constitutes the pressable portion of the option button. The selected state is active when the option button is selected. Also see ui.Elem for more details.

Derived elements

  • ui.Radio
    • Type name: radio
    • A variant of the option button that is meant to be styled like a traditional radio button rather than a pushable button.

Theming

The prelude horizontally aligns the icon image for ui.Radio to the left. There is no prelude theming for ui.Option.

Fields

In additional to all fields in ui.Elem, the following fields can be provided to the ui.Option constructor:

  • disabled (boolean): Indicates whether the option button is disabled, meaning the user cannot interact with it. Default false.
  • family (ID string): Sets the family of the option button. If none is provided, the option button works independently of any others. Default none.

The following persistent fields can also be provided:

  • selected (boolean): If present, changes the state of the option button to a new value. Newly created option buttons default to false.

The following event handlers can also be provided:

  • on_press: Fired if the option button was just pressed.
  • on_change: Fired if the value of the option button changed. Additional EventSpec fields:
    • selected (boolean): The state of the option button.

ui.Style

Styles are the interface through which the display and layout characteristics of element boxes are changed, as described in [Styles].

Constructor

  • ui.Style(sel)(props): Creates a new style object.
    • sel (SelectorSpec): The primary selector that this style applies to.
    • props (table): A table containing various fields for configuring the style. See the [Fields] section for a list of all accepted fields.
  • ui.Style(props): Same as the above constructor, but the selector defaults to * instead.

Fields

The following fields can be provided to the ui.Style constructor:

  • props (StyleSpec): The table of properties applied by this style. If this property is omitted, style properties may be inlined into the constructor table.
  • nested (table of ui.Styles): A list of ui.Styles that should be used as the cascading nested styles of this style. If this property is omitted, nested styles may be inlined into the constructor table.
  • reset (boolean): If true, resets all style properties for the selected elements to their default values before applying the new properties.

SelectorSpec

A selector is a string similar to a CSS selector that matches elements by various attributes, such as their ID, group, and state, what children they have, etc. Many of the same concepts apply from CSS, and there are a few similarities in the syntax, but there is no compatibility between the two.

Usage and syntax

A selector is composed of one or more terms. For instance, the button term selects all button elements, whereas the $hovered term selects all boxes in the hovered state. Terms can be combined with each other by concatenation to form a longer and more specific selector. For instance, the selector button$hovered selects elements that are both buttons and are hovered.

Using a comma between terms forms the union of both terms. So, button, $hovered selects terms that are either buttons or are hovered. These can be combined freely, e.g. button$pressed, scrollbar$hovered selects elements that are pressed buttons or hovered scrollbars.

The order of operations for these operations can be controlled by parentheses, so (button, check)($pressed, $hovered) is the same as the much longer selector button$pressed, button$hovered, check$pressed, check$hovered.

Note that it is not invalid for a selector to have contradictory terms. The selector #abc#xyz is valid, but will never select anything since an element can only have a single ID.

Whitespace between terms is ignored, so both button$hovered and button $hovered are valid and equivalent, although it is customary to only put whitespace after commas. The order that selectors are written in is also irrelevant, so button$hovered and $hovered button are equivalent, although the former order is preferred by convention.

Basic terms

The full list of basic terms is as follows, listed in the conventional order that they should be written in:

  • /window/ matches any element inside a window with window type window.
  • * matches every element. This is necessary since empty selectors and terms are invalid. It is redundant when combined with other terms.
  • type matches any element with the type name type. Inheritance is ignored, so elem matches ui.Elem but not ui.Label or ui.Button.
  • #id matches any element with the ID id.
  • %group matches any element that contains the group group.
  • @* matches any box within any element, not just the main box.
  • @box matches any box with the name box within any element.
  • $state matches any box currently in the state state within any element.

Box selectors

The @box selector selects a specific box to style. For instance, a selector of scrollbar%overflow@thumb can be used to style the thumb of any scrollbar with the group overflow. On the other hand, button@thumb will not match anything since buttons don't have a thumb box. Similarly, @main@thumb will not match anything since a box cannot be two separate boxes at once.

By default, a selector that contains no box selector will only match the main box. Alternatively, the special @* box selector can be used to select every box in the element. For instance, accordion@* would be equivalent to accordion(@main, @caption, @content). This selector is especially useful when performing style resets for an entire element and all the boxes within it.

Predicates

Terms can be combined with more complicated operators called predicates. The simplest predicate is the ! predicate, which inverts a selector. So !%tall selects all elements without the tall group. Note that ! only applies to the term directly after it, so !button%tall means (!button)%tall. To invert both terms, use !(button%tall).

Predicates can only work with certain types of selectors, called predicate selectors. Predicate selectors cannot use box or state selectors, since the server has no knowledge of which boxes are in which states at any given time. For example, the selector button!$pressed is invalid. On the other hand, the selector that ui.Style uses is called a primary selector, which is a selector that is allowed to use boxes and states outside of predicates.

All predicates other than ! start with the ? symbol. Simple predicates are one such predicate type, which match an element based on some intrinsic property of that element. The ?first_child predicate, for instance, checks if an element is the first child of its parent.

There are also function predicates which take extra parameters, usually a selector, to select the element. For instance, the ?<() predicate tries to match a selector sel against a parent element. So, ?<(button, check) matches all elements that have a button or a checkbox as their parent. Not all function predicates take selectors as parameters, such as ?nth_child(), which takes a positive integer instead.

List of predicates

The complete list of predicates aside from ! is as follows:

  • ?no_children matches all elements with no children.
  • ?first_child matches all elements that are the first child of their parent element.
  • ?last_child matches all elements that are the last child of their parent element.
  • ?only_child matches all elements that are the only child of their parent element.
  • ?nth_child(index) matches all elements that are at the indexth child from the start of their parent.
  • ?nth_last_child(index) matches all elements that are the indexth child from the end of their parent.
  • ?first_match(sel) matches all elements that are the first child of their parent to be matched by the selector sel.
  • ?last_match(sel) matches all elements that are the last child of their parent to be matched by the selector sel.
  • ?only_match(sel) matches all elements that are the only child of their parent to be matched by the selector sel.
  • ?nth_match(sel; index) matches all elements that are the indexth child from the start of their parent to be matched by the selector sel.
  • ?nth_last_match(sel; index) matches all elements that are the indexth child from the end of their parent to be matched by the selector sel.
  • ?<(sel) matches all elements whose parent is matched by the selector sel.
  • ?>(sel) matches all elements that have a child that is matched by the selector sel.
  • ?<<(sel) matches all elements that have an ancestor that is matched by the selector sel.
  • ?>>(sel) matches all elements that have a descendant that is matched by the selector sel.
  • ?<>(sel) matches all elements that have a sibling that is matched by the selector sel.
  • ?family(name) matches all elements that have a family of name. If name is *, then it matches elements that have a family, regardless of which one.

Note that the root element is considered to be the only "child" of the window, so it is matched by e.g. ?first_child and ?only_match(root).

Unlike CSS, there are no child or descendant combinators since the more powerful ?<() and ?<<() predicates can be used instead. Note that predicates like ?>>() should be used with care since they may have to check a large number of elements in order to see if there is a match, which may cause loss of performance for particularly large UIs.

StyleSpec

A StyleSpec is a plain table of properties for use in ui.Style or as inlined properties in ui.Elem.

Field formats

StyleSpec has specific field formats for positions and rectangles:

  • 2D vector: A table {x, y} representing a position, size, or offset. The shorthand {num} can be used instead of {num, num}.
  • Rectangle: A table {left, top, right, bottom} representing a rectangle or set of borders. The shorthand {x, y} can be used instead of {x, y, x, y}, and {num} can be used instead of {num, num, num, num}.

Fields

All properties are optional. Invalid properties are ignored.

Layout fields

  • layout (string): Chooses what layout scheme this box will use when laying its children out. Currently, the only valid option is place.
  • clip (string): Normally, a box expands its minimum size if there's not enough space for the content, but this property can specify that the content be clipped in the horizontal and/or vertical directions instead. One of none, x, y, or both. Default none.
  • scale (number): The scale factor by which positions and sizes will be multiplied by in place layouts. Default 0.
    • A scale of zero will scale coordinates by the width and height of the parent box, effectively creating normalized coordinates.

Sizing fields

  • size (2D vector): The minimum size of the box in pixels. May not be negative. Default {0, 0}.
  • span (2D vector): The size that the layout scheme will allocate for the box, scaled by scale. Default {1, 1}.
  • pos (2D vector): The position that the layout scheme will place the box at, scaled by scale. Default {0, 0}.
  • anchor (2D vector): The point which should be considered the origin for placing the box. Default {0, 0}.
    • Specified in normalized coordinates relative to the size of the box itself, e.g. {1/2, 1/2} means to position the box from its center.
  • margin (rectangle): Margin in pixels of blank space between the layout rectangle and the display rectangle. It is valid for margins to be negative. Default {0, 0, 0, 0}.
  • padding (rectangle): Padding in pixels of blank space between the middle rectangle and the padding rectangle. It is valid for padding to be negative. Default {0, 0, 0, 0}.

Visual fields

  • display (string): Specifies how to display this box and its contents. One of visible, overflow, hidden, or clipped. Default visible.

Box fields

  • box_image (texture): Image to draw in the display rectangle of the box, or "" for no image. The image is stretched to fit the rectangle. Default "".
  • box_fill (ColorSpec): Color to fill the display rectangle of the box with, drawn behind the box image. Default transparent.
  • box_tint (ColorSpec): Color to multiply the box image by. Default white.
  • box_source (rectangle): Allows a sub-rectangle of the box image to be drawn from instead of the whole image. Default {0, 0, 1, 1}.
    • Uses normalized coordinates relative to the size of the texture. E.g. {1/2, 1/2, 1, 1} draws the lower right quadrant of the texture. This makes source rectangles friendly to texture packs with varying base texture sizes.
    • The top left coordinates may be greater than the bottom right coordinates, which flips the image. E.g. {1, 0, 0, 1} flips the image horizontally.
    • Coordinates may extend past the image boundaries, including being negative, which repeats the texture. E.g. {-1, 0, 2, 1} displays three copies of the texture side by side.
  • box_frames (integer): If the box image should be animated, the source rectangle is vertically split into this many frames that will be animated, starting with the top. Must be positive. If set to one, the image will be static. Default 1.
  • box_frame_time (integer): Time in milliseconds to display each frame in an animated box image for. Must be positive. Default 1000.
  • box_middle (rectangle): If the box image is to be a nine-slice image (see https://en.wikipedia.org/wiki/9-slice_scaling), then this defines the size of each border of the nine-slice image. Default {0, 0, 0, 0}.
    • Uses normalized coordinates relative to the size of the texture. E.g. {2/16, 1/16, 2/16, 1/16} will make the horizontal borders 2 pixels and the vertical borders 1 pixel on a 16 by 16 image. May not be negative.
    • In conjunction with box_scale, this also defines the space between the display rectangle and the middle rectangle.
  • box_tile (string): Specifies whether the image should be tiled rather than stretched in the horizontal or vertical direction. One of none, x, y, or both. Default none.
    • If used in conjunction with box_middle, then each slice of the image will be tiled individually.
  • box_scale (number): Defines the scaling factor by which each tile in a tiled image and the borders of a nine-slice image should be scaled by. May not be negative. Default 1.

Icon fields

  • icon_image (texture): Image to draw in the icon rectangle of the box, or "" for no image. The image always maintains its aspect ratio. Default "".
  • icon_fill (ColorSpec): Color to fill the icon rectangle of the box with, drawn behind the icon image. Default transparent.
  • icon_tint (ColorSpec): See box_tint, but for icon_image.
  • icon_source (rectangle): See box_source, but for icon_image.
  • icon_frames (integer): See box_frames, but for icon_image.
  • icon_frame_time (integer): See box_frame_time, but for icon_image.
  • icon_scale (number): Scales the icon up by a specific factor. Default 1.
    • For instance, a factor of two will make the icon twice as large as the size of the texture.
    • A scale of 0 will make the icon take up as much room as possible without being larger than the box itself. The scale may not be negative.
  • icon_place (string): Determines which side of the padding rectangle the icon rectangle should be placed on. One of center, left, top, right, or bottom. Default center.
  • icon_overlap (boolean): Determines whether the content rectangle should overlap the icon rectangle. If icon_place is center, then they will always overlap and this property has no effect. Default false.
  • icon_gutter (number): Space in pixels between the content and icon rectangles if they are not set to overlap. It is valid for the gutter to be negative. This property has no effect if no icon image is set. Default 0.

EventSpec

An EventSpec is a plain table that is passed to event handler functions to give detailed information about what changed during the event and where the event was targeted.

An EventSpec always contains the following fields:

  • context (ui.Context): The context that the event originates from.
  • player (string): The name of the player that the window is shown to, equivalent to context:get_player().
  • state (table): The state table associated with the context, equivalent to context:get_state().

Additionally, EventSpecs that are sent to elements have an additional field:

  • target (ID string): The ID of the element that the event originates from.

EventSpecs may have other fields, depending on the specific event. See each event handler's documentation for more detail.