hafen.ui: Interactive Controls
A button, a text entry, a checkbox, a radio, a slider, a scroll, a scrollbar and a key button: the controls the user drives. Each is built bare, configured by chained setters and dressed by the stylesheet.
local harvest_window = hafen.ui():window():title("Harvest"):size(200, 100)
local search_entry = hafen.ui():entry():parent(harvest_window):position(0, 0):size(160):value("")
search_entry:on("Submitted", function(text) hafen.log():write("search: " .. text) end)
local ripe_check = hafen.ui():check():parent(harvest_window):position(0, 28):text("Only ripe"):value(true)
ripe_check:on("Changed", function(checked) hafen.store():var("cfg").ripe = checked end)
local volume = hafen.ui():slider():parent(harvest_window):position(0, 56):size(140, 20):range(0, 100):value(50)
volume:on("Changed", function(event) if event:final() then hafen.store():var("cfg").volume = event:value() end end)
A caption or a picture
A button shows text or pictures. The setter decides which.
| Method |
Returns |
Permission |
Description |
button:text(caption) |
self |
Unprotected |
A captioned button. Rewritable at any time. |
button:image(up, down [, hover]) |
self |
Unprotected |
A picture button: up at rest, down while held, hover under the cursor (defaults to up). Building-only. |
button:image() |
table \| nil |
Unprotected |
{ up =, down =, hover = } as named. nil on a captioned button. |
| Rule |
Detail |
| A face |
An image asset handle, drawn at its own pixels. Or a string naming a client image ("gfx/hud/buttons/addu"), scaled as the client scales it, so it matches the buttons beside it. |
| Building-only |
The client draws a captioned and a picture button with two different widgets, so :image chooses which widget this is. Once on screen it refuses, saying so. |
:type() |
"Button" or "IButton". :role() is button for both, so one selector matches every button you built. |
local up, down = hafen.asset():get("up.png"), hafen.asset():get("down.png")
local refresh_button = hafen.ui():button():parent(harvest_window):position(8, 8):image(up, down)
refresh_button:on("Pressed", function() hafen.log():write("refreshing") end)
Text entry
| Method |
Returns |
Permission |
Description |
hafen.ui():entry() |
Widget |
Unprotected |
A single-line text field. |
entry:value(text) |
self |
Unprotected |
The one way to write its content. Any string, "42" included (a numeric string is a string). A number is refused. Fires neither key. |
entry:value(), entry:text() |
string |
Unprotected |
The content. |
entry:text(text) |
— |
— |
Refused, naming :value(s). |
entry:on("Changed", fn) |
Sub |
Unprotected |
fn(text) on every keystroke that changes the text. |
entry:on("Submitted", fn) |
Sub |
Unprotected |
fn(text) once, on Enter, with the whole text. |
| Rule |
Detail |
| Focus |
While the field has focus a keystroke goes to it only — never to your character, a hotkey or the chat line. |
| Height |
Its field art’s, which :size(w) leaves to the control. A textentry rule whose bg is a picture, installed before the entry is built, is that art. |
| Client entries |
Submitted answers on the client’s own entries, the chat line included, and there it can be cancelled: the server never hears it — edit. |
Checkbox
| Method |
Returns |
Permission |
Description |
hafen.ui():check() |
Widget |
Unprotected |
A boolean toggle. |
check:text(caption) |
self |
Unprotected |
Its caption. |
check:value(checked) |
self |
Unprotected |
The tick. |
check:value() |
boolean |
Unprotected |
Whether it is ticked. |
check:image(up, down, hoverUp, hoverDown) |
self |
Unprotected |
Four faces, all required: the two states at rest and each under the cursor, resolved as a button’s face is. Building-only. |
check:image() |
table \| nil |
Unprotected |
{up=, down=, hoverUp=, hoverDown=}. nil on a captioned checkbox. |
check:on("Changed", fn) |
Sub |
Unprotected |
fn(checked) when the user changes it. |
:type() reads "CheckBox" or "ICheckBox". A checkbox with no picture takes the theme’s checkbox (the box) and checkbox.mark (the tick). One given pictures is dressed by neither.
Radio
| Method |
Returns |
Permission |
Description |
hafen.ui():radio() |
Widget |
Unprotected |
One control: a set of buttons of which exactly one is checked. |
radio:rows(labels) |
self |
Unprotected |
The choices, laid out in one column below the control’s :position. Writing again replaces the set and drops the checked row. :rows{} is an empty control, not an error. |
radio:value(label) |
self |
Unprotected |
Checks that row. A label not in the current set is refused, naming the rows that are. |
radio:value() |
string |
Unprotected |
The checked row. |
radio:size() |
{w=, h=} |
Unprotected |
The box of the whole stack. |
radio:on("Changed", fn) |
Sub |
Unprotected |
fn(label) when the user picks a different row. |
local sort_radio = hafen.ui():radio():rows{"Quality", "Amount", "Name"}:value("Amount")
sort_radio:on("Changed", function(pick) hafen.store():var("cfg").sort = pick end)
Slider
| Method |
Returns |
Permission |
Description |
hafen.ui():slider() |
Widget |
Unprotected |
A draggable position within a range. |
slider:range(min, max) |
self |
Unprotected |
The bounds, numbers. Narrowing re-clamps a value outside the new bounds without firing Changed. :range(nil) is refused naming the missing bound: there is no undo for a control’s own bounds. |
slider:range() |
{min=, max=} |
Unprotected |
The bounds. |
slider:value(n) |
self |
Unprotected |
The position. A value outside :range clamps to the nearer bound. "50" is refused: a value is a number. |
slider:value() |
number |
Unprotected |
The position. |
slider:on("Changed", fn) |
Sub |
Unprotected |
fn(event) on every step of the drag: event:value() the position, event:final() false while dragging and true once, on release. |
volume:on("Changed", function(event)
hafen.log():write("volume " .. event:value()) -- every step of the drag
if event:final() then hafen.store():var("cfg").volume = event:value() end -- once, on release
end)
A client slider (an options-window volume slider) reports the same key. There it is the one capability key that cannot be cancelled: the control writes its value before it reports it (edit). A slider is two surfaces, slider (the rail) and slider.knob (the thumb).
| Method |
Returns |
Permission |
Description |
hafen.ui():scroll() |
Widget |
Unprotected |
A scrolling container. Give it :size(w, h). Anything :parent()‘d into it lands in the scrolling area. |
A scrollbar appears down its right edge once the content outgrows the box and disappears when it fits. It is a control in its own right, one of scroll_box:children(). It answers :range(), :value(n) and Changed as a bare scrollbar does.
| Method |
Returns |
Permission |
Description |
hafen.ui():scrollbar() |
Widget |
Unprotected |
A bare scroll thumb, for driving something yourself. |
scrollbar:range(min, max), scrollbar:value(n) |
as a slider’s |
Unprotected |
The same bounds and position. |
scrollbar:on("Changed", fn) |
Sub |
Unprotected |
fn(value) on every step: no final flag, since a bare scrollbar has no separate drag end. |
local items, visible_rows, first_row = { "oak", "birch", "spruce", "fir", "elm" }, 3, 0
local row_scrollbar = hafen.ui():scrollbar():size(14, 160):range(0, #items - visible_rows):value(0)
row_scrollbar:on("Changed", function(value) first_row = value end)
| Rule |
Detail |
| A client scrollbar |
Reports Changed from both of its writes, the thumb drag and the wheel or step buttons, and cannot be cancelled — edit. Its rail and thumb are scrollbar and scrollbar.knob. |
| A list’s own scrollbar |
Reads its position off the list every frame, so widget:value(n) on it is refused rather than reverted. Drive the list: widget:value(row) on a listbox scrolls to that row, and the bar follows. |
The client’s own key button, the one each row of Options ▸ Game ▸ Keybindings ends in, joined to one of your hotkeys, or under client.settings to any binding your code may write. The user presses it, then the key, and the key is assigned exactly as on that row.
local keybindings = hafen.client():options():keybindings()
keybindings:on("toggle", function() hafen.log():write("toggled") end)
local toggle_binding = keybindings:binding():get("toggle") -- after on(): your own hotkey
hafen.client():options():addon():panel(function(root)
local toggle_row = hafen.ui():row():gap(8):parent(root)
hafen.ui():label():parent(toggle_row):text("Toggle the window")
hafen.ui():keybinding():parent(toggle_row):bind(toggle_binding)
end)
| Method |
Returns |
Permission |
Description |
hafen.ui():keybinding() |
Widget |
Unprotected |
A key button bound to nothing. It reads None, and a press does nothing. |
key_button:bind(binding) |
self |
Unprotected for your own hotkey. client.settings for any other binding |
Joins it to a hotkey your addon declared and has not ended: the Binding keybindings:binding():get(name) hands you. It shows that hotkey’s key at once. A second call replaces the first. |
key_button:bind() |
Binding \| nil |
Unprotected |
The Binding it is joined to, the same object keybindings:binding():get(name) hands you. nil while none. |
key_button:bind(nil) |
self |
Unprotected |
Unbinds. The key stays shown, and a press does nothing. |
key_button:value() |
string \| nil |
Unprotected |
The key it shows, spelled as binding:key() spells it. nil for unbound. |
| Rule |
Detail |
| The client’s class |
:type() reads "SetButton" and :role() button, so @SetButton names yours and the panel’s alike, and a button rule dresses it. |
| Width |
The panel’s own, 175 design px, until :size(w). The height is the button’s art. |
| Whose bindings |
Your own hotkeys, always. Another addon’s hotkey and the client’s own bindings (inv) under client.settings only: without it, each is refused naming Options ▸ Game ▸ Keybindings, where the user assigns those, and client.settings, under which the button joins it. A Binding taken before keybindings:on declared the name is the registry id as written, not yours: refused, naming the order. A hotkey ended with subscription:off() is refused. A refused bind changes nothing. |
| A hotkey that ends while bound |
Yours or another addon’s: the button keeps the Binding, and a press does nothing. Declaring the name again brings it back: the Binding is the same. |
:value(v) |
Refused, naming binding:key(key) under client.settings: the key is the binding’s. |
:text(s) |
Refused: the caption is the key. A line beside it is a label. |
:tooltip(s) |
Replaces the client’s own tip, which names Escape, Backspace and Delete. "" brings that tip back. |
| Unprotected |
Building it, binding it to your own hotkey, and pressing it. A press is the user’s own edit of their key, made by hand as on the panel’s row. binding:key(key), the write your code makes, stays under client.settings. |
| The client’s own |
The key button a row of Options ▸ Game ▸ Keybindings ends in is this class, borrowed. widget:value() reads its key there too, and :value(v) and :bind(binding) are refused: edit. |
Another addon’s hotkey, or one of the client’s
An addon that declares client.settings may already write any binding with binding:key(key). Its key buttons join the same bindings, so the user picks the key for them by hand: a setup window that sets the keys of the addons a bundle carries, or a page gathering the client’s own keys.
local keybindings = hafen.client():options():keybindings()
local chat_binding = keybindings:binding():get("addon/simple-chat/toggle") -- another addon's hotkey, by registry id
local inventory_binding = keybindings:binding():get("inv") -- one of the client's own
local setup_window = hafen.ui():window():title("Keys")
local setup_column = hafen.ui():column():gap(4):parent(setup_window)
for _, binding in ipairs({ chat_binding, inventory_binding }) do
local binding_row = hafen.ui():row():gap(8):parent(setup_column)
hafen.ui():label():parent(binding_row):text(binding:id())
hafen.ui():keybinding():parent(binding_row):bind(binding)
end
setup_window:pack()
| Rule |
Detail |
| Another addon’s hotkey |
Joins while some addon has it declared. One no addon declares right now, its addon off, not loaded or the hotkey ended, is refused naming that. One that ends while bound keeps its Binding, as your own does. |
| The client’s own |
Joins any binding the client holds, and never ends. |
| A Binding that names nothing |
Neither a hotkey of yours, nor another addon’s, nor the client’s: refused naming keybindings:binding():list(), which holds every id there is. |
| Read at the bind |
The permission is asked when :bind(binding) runs. A press is the user’s own edit, as on your own hotkey. |
A press
The user presses the key button and it reads .... The next key they press, with its modifiers, is the hotkey’s key: the same persisted, exclusive edit as the panel’s row, so the key is taken off whichever binding was assigned it.
Pressed after ... |
What happens |
| A key, with any modifiers |
Assigned. |
Escape |
Cancels. The key stays. |
Backspace |
Back to the binding’s default: unbound for an addon’s hotkey, the client’s own key for one of its bindings. |
Delete |
Unbound. |
| A modifier alone |
Nothing yet: the button waits for the key the modifier goes with. |
| Method |
Returns |
Permission |
Description |
key_button:on("Changed", fn) |
Sub |
Unprotected |
fn(key) once per press that moves the key, after the capture has closed: the key it now shows, nil for unbound. |
local key_button = hafen.ui():keybinding():bind(toggle_binding)
key_button:on("Changed", function(key) hafen.log():write("toggle: " .. tostring(key)) end)
| Rule |
Detail |
| A press that leaves the key |
Escape, the key it already had, or Backspace on a hotkey already on its default: nothing fires. |
| A change made elsewhere |
In Options ▸ Game ▸ Keybindings, on another key button, or with binding:key(key): the button shows it on its next frame, and nothing fires. |
| The same key both places |
A key assigned here shows in Options ▸ Game ▸ Keybindings, and one assigned there shows here. |
| Disabled |
A disabled key button, :enabled(false) or inside a disabled column, takes no key. |
| A capture ends by itself |
While it reads ..., disabling it, unbinding it, hiding it or ending its hotkey ends the capture: nothing is assigned and nothing fires. |
See Also
- Controls — the shared model:
:parent, :position, sizing, owned vs borrowed.
- Display — the controls with nothing to click.
- Widget — everything a control answers before it adds its own.
- Style — the rules that dress it.
- Keybindings — the hotkey a key button assigns, and the Binding it takes.