A Widget is one piece of the UI: a window, a button, a container, a label, or a surface you built. It is the one type every hafen.ui door returns.
local inventory = hafen.session():current():ui():inventory()
if inventory then
hafen.log():write(inventory:type() .. " holds " .. inventory:items():count() .. " items")
end
| Expression | Returns | Permission |
|---|---|---|
hafen.ui():window(), hafen.ui():widget() |
A surface you paint: owned, in the addon layer. | Unprotected |
A control builder (:button(), :label(), :image(), …) |
A control you built: owned, drawn by the client. | Unprotected |
session:ui():match(selector) |
The one widget matching a selector in that character’s tree, or nil. two or more raise. |
Unprotected |
session:ui():matchAll(selector) |
Every match, in tree order. An empty array, never nil. |
Unprotected |
widget:match(selector), widget:matchAll(selector) |
The same two, scoped to one widget’s subtree — searching inside one widget. | Unprotected |
session:ui():root() |
The top of that character’s tree. | Unprotected |
hafen.ui():root() |
The top of the addon layer: the parent of every surface built without :parent(w), every addon’s. |
Unprotected |
session:ui():node(id) |
The widget for a server widget id in that character’s tree, or nil. |
Unprotected |
session:ui():inventory() |
That character’s main backpack grid. | Unprotected |
session:ui():equipment() |
That character’s worn-equipment grid. | Unprotected |
session:player():hand() |
That character’s cursor and the Item on it. |
Unprotected |
hafen.ui():hit(x, y) |
The deepest widget under a root-coordinate point — hit-testing. | Unprotected |
hafen.ui():tipAt(x, y) |
The widget whose tooltip the client shows at that point, or nil — tooltips. |
Unprotected |
hafen.ui():mouse() |
The pointer, which is not a Widget — mouse. | Unprotected |
session is a Session: hafen.session():current() for the character on screen, hafen.session():get(user) for any other. A widget the client put up belongs to one character. A session nobody is looking at keeps its whole tree, so its windows stay findable from another character, and a mirror shows any widget of it. What you built is in no character’s tree. It stands in the addon layer, whose top is hafen.ui():root().
A Widget is opaque userdata, interned per addon. Two lookups of one live widget are the same Lua value. == is the identity test, a Widget works as a table key, and a handle kept across frames compares next frame. Holding a handle keeps nothing alive.
local mouse, session = hafen.ui():mouse(), hafen.session():current()
assert(hafen.ui():hit(mouse:x(), mouse:y()) == hafen.ui():hit(mouse:x(), mouse:y())) -- one object
assert(session:ui():inventory() == session:ui():node(session:ui():inventory():id())) -- one widget, one object
A widget that left the tree (window closed, server destroy, relog) is stale. Every read answers nil or empty. Every client-side write is a no-op that still chains. :exists() is false.
| Case | Behaviour |
|---|---|
A stale Widget passed as an argument (widget:parent(p), :draggable(h), :resizable(h), :replace(view), a rule’s anchor{ to = w }) |
Nothing is placed, armed or installed. The call chains. The matching read answers nil. |
| A value that is not a Widget passed where one is expected | Raises, naming what a Widget is. |
A stale receiver on widget:send(msg, ...) |
Raises: there is no tree to deliver into. |
A stale receiver on widget:on(key, fn) |
Raises, naming the missing tree. |
A stale receiver on widget:match(sel), widget:matchAll(sel) |
Raises: an empty answer would read as “no match”. |
A stale receiver on widget:overlay():add(key) |
Raises: nothing is left to draw over. |
Every method answers on every widget, owned or borrowed, and none throws.
| Method | Returns | Permission | Description |
|---|---|---|---|
:type() |
string |
Unprotected | Class name ("Inventory", "Label"). For an anonymous subclass, the nearest named superclass. |
:role() |
string \| nil |
Unprotected | What it is in the selector vocabulary. nil when nothing classifies it. |
:res() |
string \| nil |
Unprotected | Its resource name, e.g. "gfx/invobjs/torch". On a building site’s material box (@ISBox) the material it counts. nil for most widgets. |
:picture() |
string \| nil |
Unprotected | The resource name of the picture it shows, e.g. "gfx/hud/wnd/lg/cbtnu" on a window’s close button. nil where it shows none. A different read from :res(). |
:id() |
number \| nil |
Unprotected | Server widget id. nil when the widget is not server-bound. |
:session() |
Session \| nil |
Unprotected | The character whose tree it stands in. nil for one in the addon layer. |
:events() |
string[] |
Unprotected | The event keys this widget answers. |
:name() |
string \| nil |
Unprotected | The name the addon that built it gave it, as <addon>/<name> — naming your own. |
:stock() |
table \| nil |
Unprotected | What a widget you built declared its own look to be — the same page. |
:owned() |
boolean |
Unprotected | Whether your addon built it — owned vs borrowed. |
:addon() |
string \| nil |
Unprotected | The id of the addon that built it, "(console)" for the :lua console’s. nil for one no addon built. An argument raises. |
:is(selector) |
boolean |
Unprotected | Whether this widget matches the selector. |
:children() |
collection | Unprotected | Child Widgets in tree order, back to front: the last is painted over the rest. Empty for a leaf. :list() is the array. A child has no key, so there is no :get. |
:parent() |
Widget \| nil |
Unprotected | The enclosing widget. nil at the root, and nil inside the Kin window. |
:position() |
{x=, y=} |
Unprotected | Position within the parent, in design pixels. :position(x, y) moves it. |
:size() |
{w=, h=} |
Unprotected | The box :size(w, h) writes, in design pixels: a window’s content area, any other widget’s whole box. The frame’s own box is :chrome().frame. .x on a size raises. |
:visible() |
boolean |
Unprotected | Whether it is drawn. :visible(b) writes it. |
:z() |
number \| nil |
Unprotected | The band it stands in among its siblings, 0 unless something put it elsewhere. :z(n) writes it, and :raise()/:lower() order it within the band. |
:enabled() |
boolean |
Unprotected | Its own input flag (:enabled(b)). true when built, true on every client widget. true on a child of a disabled column, where the column reads false. |
:draggable() |
Widget \| nil |
Unprotected | The handle your addon armed for the user to drag it by — :draggable(h). |
:resizable() |
boolean \| Widget \| nil |
Unprotected | true while the client’s grip is on a window of yours, else the handle your addon armed, else nil — :resizable(h). |
:remember() |
string \| nil |
Unprotected | The name your addon keeps its place and box under — :remember(name). |
:text() |
string \| nil |
Unprotected | Best-effort text of a text-bearing widget (Label, Button, CheckBox, Window, TextEntry, an @ISBox’s have/total figure). :text(s) writes it on a control you built or one of the client’s. |
:tooltip() |
string \| nil |
Unprotected | The line shown when the pointer rests on it — tooltips. |
:focused() |
boolean |
Unprotected | Whether a keystroke reaches this widget — focus. |
:image() |
table \| nil |
Unprotected | The faces of a control that shows pictures, {up=, down=, hover=}. nil otherwise. |
:value() |
any \| nil |
Unprotected | What a control holds, the client’s own included. nil where it holds nothing. :value(v) writes it on one you built and, protected, on one of the client’s. |
:source() |
string \| userdata \| nil |
Unprotected | The picture a picture control shows. nil before one is set. |
:rows() |
table \| nil |
Unprotected | The row source of a radio or a row-source control. On one of the client’s lists, every row it holds. nil where a control has no rows. |
:range() |
{min=, max=} \| nil |
Unprotected | The bounds of a slider or scrollbar. |
:gap() |
number \| nil |
Unprotected | The room between the children of a column or row, in design pixels. nil elsewhere. |
:rowHeight() |
number \| nil |
Unprotected | The row height of a listbox, dropdown, menu or table, in design pixels. |
:cellSize() |
{w=, h=} \| nil |
Unprotected | The cell box of a grid, in design pixels. |
:columns() |
table \| nil |
Unprotected | The column descriptors of a table. |
:item() |
Item \| nil |
Unprotected | The item an icon draws — a container’s cell, the cursor, a recipe slot, a listing a resource paints. nil on every other widget. |
:group() |
number \| nil |
Unprotected | The group a list row draws its name in, 0..254. A kin roster row answers the kin’s own group (kin:group()). A village’s or realm’s member row answers that polity’s group for them, also for a member the roster does not know (???). nil on every other widget and on a polity with no groups. |
:row() |
Row \| nil |
Unprotected | On a row widget of one of the client’s lists: the row it draws. nil on every other widget. |
:search() |
string \| nil |
Unprotected | What one of the client’s search lists is filtered by. nil while no search runs, and on every other widget. :search(text) filters it. |
:items() |
collection of Item |
Unprotected | The items inside it — items. |
:exists() |
boolean |
Unprotected | Whether it is still in the tree. |
:info() |
table \| nil |
Unprotected | Snapshot {type, role, res, id, pos, size, visible, enabled, text, owned, addon}. Absent values are unset keys. nil once stale. |
:walk(fn) |
self |
Unprotected | Depth-first visit, fn(widget, depth). Return false to prune that subtree. |
:hit(coord) |
Widget \| nil |
Unprotected | The deepest widget under a {x=, y=} root-coordinate point within this subtree. |
:rootPos() |
{x=, y=} \| nil |
Unprotected | Its top-left in root coordinates. With :size() (a window: :chrome().frame) it gives the rectangle outlining it. |
:replacement() |
Widget \| nil |
Unprotected | The view you put in place of this widget’s window — replace. |
:chrome() |
table \| nil |
Unprotected | On a window wearing the client’s decoration: frame = {w=, h=} (the outer box) and content = {x=, y=, w=, h=} (the content area, from the frame’s top-left), always. caption = {x=, y=}, plate = {x=, y=, w=, h=, styled=}, sizer = {x=, y=}, close = {x=, y=, w=, h=} once each ornament has drawn. nil on anything else. |
:overlay() |
collection | Unprotected | What your addon draws over this widget: keyed painters and labels, clipped to its box, ending with it. |
:style() |
table \| nil |
Unprotected | The style this widget resolves to. nil when nothing names it. |
:rule() |
Rule |
Unprotected | Your own level of the cascade on this widget: setters, :info(), :release(). |
Reading the tree is unprotected client data. :id() is what makes a widget bound: a server-placed widget has one, a widget you built does not, and only a bound widget can :send.
hafen.session():current():ui():root():walk(function(node, depth)
hafen.log():write(string.rep(" ", depth) .. node:type()
.. (node:role() and (" [" .. node:role() .. "]") or "")
.. (node:id() and (" #" .. node:id()) or "")
.. (node:text() and (" '" .. node:text() .. "'") or ""))
end)
| Method | Returns | Permission | Description |
|---|---|---|---|
:match(selector) |
Widget \| nil |
Unprotected | The one match inside this widget’s subtree, itself included. nil for none. Two or more raise. |
:matchAll(selector) |
Widget[] |
Unprotected | Every match inside it, in tree order. Empty, never nil. |
The scope decides the candidates. The selector is matched against the whole tree, so an ancestor step may name a widget above the scope. The contract is stated once, under inside one widget.
Any widget — built, found by selector, or handed to you by an event — answers :on(key, fn). The call returns a subscription, ended with :off(), and never sits mid-chain.
local subscription = hafen.session():current():ui()
:match("window[title=Cupboard] inventory"):on("MouseDown", function(event)
if event:button() == 3 then event:preventDefault() end -- right-click disabled on this cupboard only
end)
subscription:off()
| Key | event answers |
Cancelable | Fires |
|---|---|---|---|
MouseDown |
:x() :y() :button() :preventDefault() |
Yes | A mouse button is pressed over it. |
MouseUp |
:x() :y() :button() :preventDefault() |
Yes | A mouse button is released over it. |
MouseMove |
:x() :y() :preventDefault() |
Yes | The pointer moves over it. |
Wheel |
:x() :y() :amount() :preventDefault() |
Yes | The wheel turns over it. |
Removed |
— | No | It leaves the tree on its own. See below for descendants. |
Dragged |
:x() :y() |
No | The user finished dragging it by a handle you armed. |
Resized |
:w() :h() |
No | The user finished resizing it by a handle you armed or by the client’s grip. |
| Rule | Detail |
|---|---|
| Coordinates | event:x()/:y() are widget-local design pixels. On a window you painted, local means the content area (where a press lands). On a client window, its own box, caption included. |
| Button and wheel | event:button() is 1 (left) or 3 (right), on MouseDown/MouseUp only. event:amount() is the wheel delta. |
| Cancelling | event:preventDefault() stops the input reaching the widget’s own handling and any child under it. No handler’s return value is read. Two handlers fire independently. Either cancelling cancels, both still run. |
| Controls | A control answers these keys plus its own capability keys (Pressed, Changed, Submitted, Selected, Cell) — controls. On a client control the capability key is cancelable (edit), except a borrowed slider’s or scrollbar’s Changed, where preventDefault() raises: the value has already moved. A client search list also answers Search, once per row as it searches. |
| Surfaces | A surface you paint answers Draw, Update, Drop and Close on top — custom. |
| Unknown key | Raises, listing the keys this widget answers: a Label has no event 'Pressed' — it has: MouseDown, MouseUp, MouseMove, Wheel, Removed, Dragged, Resized. |
Removed scope |
Fires for the widget that was destroyed, not for what was inside it: a control that dies with its window fires nothing. Subscribe on the window. widget:exists() answers for any widget at any moment. |
| Release | A subscription on a client widget ends on :reload, on disable, or with widget:revert() on the widget or an ancestor. The client’s widget survives untouched. |
A widget is owned when your addon created it: a surface, a column, a control. It is borrowed otherwise: a client widget or another addon’s. Every read answers on both. Which writes answer on which is one table on writes. disabling a widget is the one write with no borrowed half.
| Method | Returns | Permission | Description |
|---|---|---|---|
widget:send(msg, ...) |
self |
widget.send |
Sends a widget message from a server-bound widget. msg is a string (a number is refused). Trailing arguments marshal as an action’s event:args() reads them: {x=, y=} becomes a coordinate. Numbers, strings and booleans pass through. |
hafen.session():current():ui():match("@MapView"):send("click", {x = 0, y = 0}, {x = 0, y = 0}, 1, 0)
| Case | Result |
|---|---|
Without widget.send in the manifest |
Raises, naming the key. The gate runs before the arguments are read. |
| A widget your addon built (no server id) | Refused, naming that. :id() answers the same question beforehand. Send from the nearest bound ancestor instead. |
| A stale widget | Raises. Nothing is sent. |
The receiver is the target: there is no address argument. widget.send is the widest of the keys that reach the game server, any message the client itself could send. Only console.run is wider. Declare it only where no typed verb covers the message.
| Method | Returns | Permission | Description |
|---|---|---|---|
widget:tooltip() |
string \| nil |
Unprotected | The line shown when the pointer rests on it. A plain string as given, or the text of a client keybound tip (the shortcut appended is the keymap’s), or nil. Answers on any widget. Never throws. |
widget:tooltip(text) |
self |
Unprotected | Writes it on a control you built. "" clears it. Markup follows g:text: plain text takes the stock path. $col[r,g,b]{…}, $b, $i, $u, $size, $font render as the client’s own button tips do (wrapped, keybinding appended). Malformed markup is shown literally. :tooltip() reads the string as written. |
hafen.ui():tipAt(x, y) |
Widget \| nil |
Unprotected | The widget whose tooltip the client shows at a root-coordinate point. A tooltip is inherited from the nearest ancestor carrying one, so this is not always hafen.ui():hit(x, y). Both resolve panels standing in the 3D world. |
widget:focused() |
boolean |
Unprotected | Whether a keystroke reaches this widget. Focus is a path from the root: true for the text entry being typed into and for the window around it. Read-only. An argument is refused. |
local mouse = hafen.ui():mouse()
local speaker = hafen.ui():tipAt(mouse:x(), mouse:y())
if speaker then hafen.log():write(speaker:tooltip()) end
:items(), :overlay(), :rule() and :style().