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

2088 lines
86 KiB
Markdown
Raw Normal View History

2024-09-12 16:11:41 -07:00
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.
```lua
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:
```lua
-- 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:
```lua
-- 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`.
```lua
-- 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.
```lua
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.
```lua
-- 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()`.
```lua
-- 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.
```lua
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.
```lua
-- 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.
```lua
-- 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:
```lua
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**: Scrollbars are not implemented yet, so this documentation only
> 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:
```lua
-- 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.
```lua
-- 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.
```lua
-- 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.
```lua
-- 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:
```lua
-- 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()`:
```lua
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].
```lua
-- 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:
```lua
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:
```lua
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):
```lua
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.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 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.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.
* `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.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 of `ui.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 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.Style`s): A list of `ui.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 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 `index`th child
from the start of their parent.
* `?nth_last_child(index)` matches all elements that are the `index`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 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 `index`th 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 `index`th
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, `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.