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 typesgui
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:
- A
SelectorSpec
that decides which boxes and states to apply the style to. - A
StyleSpec
that contains the actual styling properties. - 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 ofsize[<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 amod_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 tocontext:get_player()
.state
(table): The state table associated with this context, equivalent tocontext: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.
- Returns
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.
- Returns
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 byui.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 byui.get_default_theme()
.styles
(table ofui.Style
s): 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 byui.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 withallow_close
set to false. Additionally, the player leaving the game andui.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. AdditionalEventSpec
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.Elem
s 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.Elem
s 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.
- Type name:
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.
- Type name:
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 thebox_image
property.
- Type name:
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 withui.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'smain
box without any state selectors applied. If this property is omitted, these style properties may be inlined into the constructor table.styles
(table ofui.Style
s): 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 ofui.Elem
s): 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 themain
box.main
(static): The main box. Seeui.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 seeui.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. Theselected
state is active when the toggle button is selected. Also seeui.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.
- Type name:
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.
- Type name:
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. AdditionalEventSpec
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. Theselected
state is active when the option button is selected. Also seeui.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.
- Type name:
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. AdditionalEventSpec
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 ofui.Style
s): A list ofui.Style
s 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 typewindow
.*
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 nametype
. Inheritance is ignored, soelem
matchesui.Elem
but notui.Label
orui.Button
.#id
matches any element with the IDid
.%group
matches any element that contains the groupgroup
.@*
matches any box within any element, not just themain
box.@box
matches any box with the namebox
within any element.$state
matches any box currently in the statestate
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 theindex
th child from the start of their parent.?nth_last_child(index)
matches all elements that are theindex
th 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 selectorsel
.?last_match(sel)
matches all elements that are the last child of their parent to be matched by the selectorsel
.?only_match(sel)
matches all elements that are the only child of their parent to be matched by the selectorsel
.?nth_match(sel; index)
matches all elements that are theindex
th child from the start of their parent to be matched by the selectorsel
.?nth_last_match(sel; index)
matches all elements that are theindex
th child from the end of their parent to be matched by the selectorsel
.?<(sel)
matches all elements whose parent is matched by the selectorsel
.?>(sel)
matches all elements that have a child that is matched by the selectorsel
.?<<(sel)
matches all elements that have an ancestor that is matched by the selectorsel
.?>>(sel)
matches all elements that have a descendant that is matched by the selectorsel
.?<>(sel)
matches all elements that have a sibling that is matched by the selectorsel
.?family(name)
matches all elements that have a family ofname
. Ifname
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 isplace
.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 ofnone
,x
,y
, orboth
. Defaultnone
.scale
(number): The scale factor by which positions and sizes will be multiplied by inplace
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 byscale
. Default{1, 1}
.pos
(2D vector): The position that the layout scheme will place the box at, scaled byscale
. 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.
- Specified in normalized coordinates relative to the size of the box
itself, e.g.
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 ofvisible
,overflow
,hidden
, orclipped
. Defaultvisible
.
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.
- Uses normalized coordinates relative to the size of the texture. E.g.
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.
- Uses normalized coordinates relative to the size of the texture. E.g.
box_tile
(string): Specifies whether the image should be tiled rather than stretched in the horizontal or vertical direction. One ofnone
,x
,y
, orboth
. Defaultnone
.- If used in conjunction with
box_middle
, then each slice of the image will be tiled individually.
- If used in conjunction with
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
): Seebox_tint
, but foricon_image
.icon_source
(rectangle): Seebox_source
, but foricon_image
.icon_frames
(integer): Seebox_frames
, but foricon_image
.icon_frame_time
(integer): Seebox_frame_time
, but foricon_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 ofcenter
,left
,top
,right
, orbottom
. Defaultcenter
.icon_overlap
(boolean): Determines whether the content rectangle should overlap the icon rectangle. Ificon_place
iscenter
, 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 tocontext:get_player()
.state
(table): The state table associated with the context, equivalent tocontext:get_state()
.
Additionally, EventSpec
s that are sent to elements have an additional field:
target
(ID string): The ID of the element that the event originates from.
EventSpec
s may have other fields, depending on the specific event. See each
event handler's documentation for more detail.