Brodgar client

hafen.ui: Selectors

A selector is a CSS-shaped string naming a piece of the client’s UI. session:ui():match and :matchAll are the lookups, and the same string is the key of a stylesheet rule.

local session = hafen.session():current()
session:ui():match("window[title=Cupboard]")                 -- the one match, or nil
session:ui():matchAll("inventory")                           -- every open container, in tree order
session:ui():match("window[title=Cupboard] inventory")       -- the grid inside that window
session:ui():match("window[title=Foo] button[text=Close]")   -- one exact widget, in one string
session:ui():match("@Equipory")                              -- by class
session:ui():matchAll("*[res*=gfx/hud/meter]")               -- by resource name, as a substring
session:ui():root()                                          -- no selector: the whole tree

A lookup searches one character’s tree, so it is addressed at that character’s session. hafen.session():current() is the one on screen, hafen.session():get(user) any other. A tree answers the same searches whether or not it is drawn. Windows your addon builds stand in the addon layer, out of every character’s tree. hafen.ui():root():match(selector) searches the layer.


Lookups

Method Returns Permission Description
session:ui():match(selector) Widget \| nil Unprotected The one match in that character’s tree. No match is nil. Two or more raise, saying how many matched and naming matchAll(selector)[i].
session:ui():matchAll(selector) Widget[] Unprotected Every match, in tree order. Empty, never nil.
session:ui():root() Widget \| nil Unprotected The top of that character’s tree.
hafen.ui():root() Widget Unprotected The top of the addon layer, every addon’s surfaces under it. Its :match and :matchAll search the layer. An argument raises.
widget:match(selector), widget:matchAll(selector) as above Unprotected The same search over widget’s own subtree, widget included — below.

One, or all of them

:match is for a selector that identifies one widget: a chain down to an exact button, a unique caption, a class only one widget has. A selector naming several today may name one on your screen: “the Cupboard window” is one widget until a second cupboard opens. A lookup that picked whichever came first would act on the wrong one. The raise is the answer instead. :matchAll is for a question with several answers.

Inside one widget

widget:match(sel) and widget:matchAll(sel) run the same grammar with the same strictness over one widget’s subtree. The scope decides the candidates. An ancestor step may still name a widget above it, as in CSS. Inside an Added callback it is the right lookup. The whole tree has no single answer to window[title=Cupboard] inventory while two cupboards are open. The widget you were handed does.

session:ui():on("window[title=Cupboard]", "Added", function(cupboard)
  local grid = cupboard:match("inventory")       -- this cupboard's grid, whatever else is open
end)

Both refuse on a widget that has left the tree: “nothing matched” and “the thing searched is gone” are different answers. Ask :exists() first when unsure.


The grammar

A selector is one or more steps separated by spaces. A step is a role or *, refiners, or both, in any order, each refiner at most once. Refiners alone are a step (@Equipory, [res*=gfx/hud]).

Part Matches
* Any widget, including one no role classifies.
A role What the widget is — roles.
@Class Its class name, the string :type() reports: the nearest named class, so @Window matches a plain Window and not a CharWnd. Use the window role for any window.
[title=…] A window’s own caption. Valid only in a step whose role is window.
[text=…] The words a widget displays: a button’s caption, a label, a checkbox’s label, an entry’s contents. Refused on a window step, and never matches a window under *.
[res=…] Its resource name, :res() — what carries one.
[name=…] What the addon that built it calls it, as <addon>/<name> — below.
Operator Matches when the value
= Is exactly this.
*= Contains this.
^= Starts with this.
$= Ends with this.
Rule Detail
The space The descendant combinator: window[title=Cupboard] inventory is the grid anywhere below the Cupboard window. It is the only part that looks upward. Every refiner tests its own step. There is no >.
Where [title=] goes The client wraps bare widgets in titled windows, so the grid is window[title=Cupboard] inventory. inventory[title=Cupboard] is refused and the error hands back that spelling. Under CSS it would parse and never match.
Captions Match the client’s own English whatever it displays: a catalogue lands at the render, so [title=Inventory] matches a window drawn in another language.
Errors Anything else raises naming the offending part, listing every valid role for a bad one. The same error comes back for an invalid sheet key.
The Macros page Matches no selector, nor does any widget in it or its Delete question: it is outside every addon’s reach. A count of matches never includes it.

The one refiner an addon owns

Every other part describes what a widget happens to be. [name=…] is what an addon called a widget it built, through :name(word). Without it every bare surface reports @AddonWidget and nothing tells one addon’s bar from another’s meter.

local bar = hafen.ui():widget():name("bar")                  -- in the addon, once
{ "rules": {
    "[name=myaddon/bar]":   { "bg": {"color": [9, 13, 22, 214]} },
    "[name^=myaddon/slot]": { "bg": {"asset": "themes/cyberpunk/slot.png"} }
} }
Rule Detail
The addon’s id is written in front :name("bar") is [name=myaddon/bar]. Two addons naming a bar cannot collide.
Weight 16 Above [res=]’s 8: the only part an author chose. [name^=…] and [name=…] weigh the same and a JSON theme has no key order, so an exception is a chain, which sums: [name=myaddon/bar] [name=myaddon/slot7] is 32.
Operators Every one, so slot1…slot12 are dressed as a group by ^= and one of them by =. Naming twelve surfaces the same is legal and buys nothing the prefix does not.
Written once A name is identity, not state. The states a surface enters are inside a rule’s value.
Owned only Naming a client widget is refused, naming widget:rule(). A name nobody answers to matches nothing and is not an error.

Roles

:role() answers what a widget is, or nil. The names are the stylesheet’s site keys: every site key is a role, and item, column, row and the regions’ roles classify a widget and name no site. hafen.ui():role() is the collection of every role the client publishes, each answering :name() and :selector(). The selector is the same string for a role that matches a widget, nil for a site role.

Role Matches
window Window and every subclass, the Hidewnd around the inventory included.
inventory Inventory and Equipory: every open container.
button Button, IButton.
label Label.
textentry TextEntry.
chat ChatUI and its channels.
menu MenuGrid, FlowerMenu.
item The icon one item is drawn as, wherever it is. A container’s cell, an equipment slot, the cursor’s item, a recipe’s input or output slot, an icon a resource ships its own widget for. Exactly the widgets widget:item() answers on.
column, row A column or row an addon built.
fight.opening.mine, fight.opening.theirs, fight.ip.mine, fight.ip.theirs, fight.cooldown, fight.last.mine, fight.last.theirs, fight.action The region standing for that element of the combat display, while a fight is drawn.
hud.cmdline, hud.message, hud.chat The region standing for the command line, the last notice or the hidden chat’s lines, bottom left, while the HUD stands.
Rule Detail
Site roles They name a place the client draws, not a widget it places. A window’s parts: window.title, window.frame. Boxes and text: panel, heading, tooltip, inventory.slot, world.nick, world.speech. The parts of a control: checkbox, checkbox.mark, slider, slider.knob, scrollbar, scrollbar.knob, meter. The chat kinds: chat.mine, chat.private, chat.system and the rest. The HUD’s plates: hud.belt, hud.menu.left, menu.slot, minimap.frame and the rest.
A site role matches nothing session:ui():match("checkbox") is accepted and answers nothing. role:selector() is nil for exactly these.
No role Most widgets: layout containers, scroll ports, images, a bare surface of yours. Reach them with *, @Class, [res=], [name=], or a chain on their window.

What carries a res

[res=] is the stable key: a resource name never changes with the client’s language. Only some widgets have one. A container’s cell (gfx/invobjs/…, its item’s). Meters (gfx/hud/meter/hp). A building site’s material boxes (@ISBox, the material each counts). Widgets whose code ships in a resource (ui/rchan, ui/vlg). Most windows carry none: [res=] for items and meters, [title=] for windows. widget:res() tells you.

Rule Detail
A path wants *= [res*=gfx/hud/meter] catches every meter. [res=gfx/hud/meter] matches nothing.
Depictions An icon drawing a depiction carries the resource its own code came from, or none. Refine those on the item, where widget:item():res() answers for every icon.
A [res=] rule lands within about twenty frames A widget’s resource arrives with no event, so a rule that did not match is re-asked for a bounded run of frames and then settles. A resource resolving later never starts matching for that widget. Installing the sheet again, or any edit to it, re-opens every answer.

The picture is a different read

:res() names the resource a widget’s code came from. Most of the client’s chrome is a Java class showing a piece of the game’s art, and widget:picture() names that art where :res() is nil. :picture() has no selector key.

local sample_window = hafen.ui():window():title("Sample")
local close_button = sample_window:match("@IButton")   -- the close box the client put in the chrome
close_button:picture()                                 -- "gfx/hud/wnd/lg/cbtnu"
close_button:res()                                     -- nil

:picture() answers on a widget that holds a picture: a picture control, a picture button, a picture checkbox. It answers nil on one that composes or paints its art, such as an inventory square or a meter’s bar. It answers nil for a picture out of your own asset file. Two widgets sharing one picture name one resource. Any argument raises.


Hit-testing

Method Returns Permission Description
hafen.ui():hit(x, y) Widget \| nil Unprotected The deepest widget under a root design-pixel point.
hafen.ui():mouse():over() Widget \| nil Unprotected The same, at the pointer.

Both mirror the client’s pointer dispatch, so they return the widget a click would hit. Children are taken topmost-first, in the order :raise(), :lower() and :z(n) leave them, invisible widgets skipped, scroll offsets followed, non-rectangular hit areas honoured. A region is passed over, as a click passes it. A position-plus-size rectangle test is wrong inside scrolled lists. Walk :parent() up for the stack. :rootPos() and :size() give the outline.

local last                                             -- the leaf the stack was last built for
hafen.event():on("Update", function(delta)
  local leaf = hafen.ui():mouse():over()
  if leaf == last then return end                      -- hover unchanged: no walk, no rebuild
  last = leaf
  local stack, node = {}, leaf
  while node do stack[#stack + 1] = node; node = node:parent() end
end)

Widgets are interned, so the guard is a plain == and covers nil == nil. Hovering a window’s frame resolves to its decoration (@DefaultDeco, role nil), a child widget of its own. The window is one level up, or window[title=…].

Hold the result

Every lookup walks its whole scope and parses the selector on every call. Hold the widget the lookup answered, not the string. Widgets are interned, holding one is free, and :exists() says whether it is still there. There is no cache underneath.

The inspector

The widgetstack addon reports the hovered widget’s role, class, own [title=] or [text=], [res=] and anchor. The anchor is the nearest captioned window, as the first step of a chain. Then it lists every selector it can build from those parts and resolve back to the widget. The most specific comes first, each with how many widgets it matches. Chains come first, since they name one widget. [res*=…] on a path’s last segment and the ^= form of a value before its first digit are offered where = would not hold. The bottom line is ready for :lua: hafen.session():current():ui():match("…") when it matches this widget alone, …:matchAll("…")[i] otherwise. Under it, every read with something to say prints a line. A control’s own configuration (:range(), :rowHeight(), :cellSize(), :columns(), :source(), :image()) is the builder’s and says nothing on a client control. :rows() says something on a client list too: every row it holds. Click a row or run :selector to log the line. The freeze hotkey holds the stack.


See Also