Brodgar client

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).

Scroll

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.

Scrollbar

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.

Key button

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