The unprotected writes that answer on a widget you do not own: :position(x, y), :size(w, h), :raise(), :lower(), :z(n), :parent(p), :visible(false), :draggable(h), :resizable(h) and :remember(name). Each records what it found, so everything is given back when your addon goes.
local session = hafen.session():current()
local inventory = session:ui():match("window[title=Inventory]")
local grip, corner = hafen.ui():widget():size(24, 24), hafen.ui():widget():size(12, 12)
inventory:position(40, 200) -- move it
inventory:size(300, 220) -- resize its content; the chrome refits around it
inventory:position(nil) -- drop your move: back to where the user had it
inventory:draggable(grip) -- let the user move it, by pressing a widget of yours
inventory:resizable(corner) -- and size it, by pressing another
inventory:remember("bag") -- and have it come back there next session
:destroy() stays refused on a widget you do not own — owned vs borrowed. Changing what a client widget says or does is edit. Standing one in the 3D world is hafen.virtual, restoring under the same rule.
Moving and resizing (unprotected)
| Method |
Returns |
Permission |
Description |
widget:position(x, y) |
self |
Unprotected |
Moves it, writing the field the user’s own drag writes: what you place is what you click. On the HUD or in the layer the place follows the screen (A place on the screen follows the screen, below). |
widget:size(w, h) |
self |
Unprotected |
Resizes its content. A window’s chrome refits. |
widget:position(nil), widget:size(nil) |
self |
Unprotected |
Drop your level of that half. The widget lands on a rule that still names it, else on the stock: what the client recorded on your first touch, or the builder’s default place and box on a surface of yours. A window on the HUD or in the layer that follows the screen lands at the user’s place for the size the screen is now (Re-layout, below). The chat lands where the client places it now (The chat, below). |
widget:position() |
{x=, y=} |
Unprotected |
Within the parent, in design pixels. The HUD is not the root, so :rootPos() is the screen coordinate. |
widget:size() |
{w=, h=} |
Unprotected |
The box :size(w, h) writes — a window’s content area — so writing a size back is a no-op. The frame is :chrome().frame. |
| Rule |
Detail |
| A layer, never a write into the client |
The first touch records what the widget was. :reload and disable give back everything you held. A relog restores nothing: that session’s widgets are gone. |
| The disk is the user’s |
The client persists the places of its own windows: inventory, equipment, character sheet, kin, map, action search, icon settings, crafting, an item’s contents, windows tracked by id. Each is saved relative to the screen, so it opens at the same relative place at another window size or interface scale. It always writes what the user last placed, never your level. Uninstalling your addon leaves the HUD as its owner arranged it. |
| The belt |
The client puts the numbered belt above the chat every frame. Held, by :position(x, y), a rule, :remember or a drag, it stays where you put it, and the command line, the notice and the hidden chat’s lines no longer stand above it. Taken into a surface of yours, it is left where it stands. A chat taken out of the HUD stops counting too, and the belt takes its place bottom left. |
| A position always lands. A size may not |
A window that packs itself around its contents (the main inventory) honours :size(w, h) and undoes it before the call returns: inert, never an error. Read :size() back to tell. The sheet’s padding follows the same rule. |
| A place on the screen follows the screen |
On a widget directly on the HUD or in the layer, :position(x, y) is kept as where the widget’s centre landed, a fraction of the screen, clamp included. Resizing the game window moves it with the screen, by the rule Re-layout states, edges included: a widget placed against an edge stays on it. A :size(w, h) of yours takes the fraction again where the widget then stands. :position() reads where it stands now. Inside a window a place stays in pixels of that window. |
| A widget covering the screen keeps its pixels |
A widget at least as large as its parent on both axes, the HUD or the layer, has no free room to keep a fraction of. Its place stays in pixels, through a resize of the game window and through :remember. A widget covering one axis only follows the screen. |
| The chat |
:position(x, y) places the chat’s top-left, shown or hidden. Written while the chat is hidden, by Ctrl+C or by :visible(false), the place is where the chat shows. :position() reads that same place either way. :position(nil) puts it where the client places it now, its bottom on the HUD’s bottom and the whole of it on the screen, at the screen’s size and the chat’s height of that moment. |
| Two addons |
Each may hold a layer on one widget. The last write wins on screen, each restores what it found. |
| The cascade |
The verb is the top level over a sheet’s position and size rules. :position(nil) drops your level and falls back to a rule that still names the widget. It reaches the stock value only when none does. A surface of yours runs the same cascade, its stock the builder’s default place and box. |
Front and back (unprotected)
widget:raise() and widget:lower() put a widget in front of or behind its siblings. widget:z(n) names the band they move it within, and is given back like a place.
| Method |
Returns |
Permission |
Description |
widget:raise() |
self |
Unprotected |
Puts it in front of every sibling of its band. An act: nothing is recorded or given back, as when the user presses a window and it comes to the front. |
widget:lower() |
self |
Unprotected |
Puts it behind every sibling of its band. An act, as :raise(). |
widget:z(n) |
self |
Unprotected |
Stands it in band n, a whole number from -9 to 9: in front of every sibling of a lower band, behind every one of a higher. A level, held through every later raise. |
widget:z() |
number \| nil |
Unprotected |
The band it stands in, whoever put it there. 0 for a widget nothing has put elsewhere. nil once stale. |
widget:z(nil) |
self |
Unprotected |
Drops your band. The widget goes back to the band it had and behind the sibling it followed on your first :z(n). |
local toolbar = hafen.ui():widget():size(200, 40):position(20, 20)
toolbar:z(5) -- over every surface in band 0, however often the user presses them
toolbar:raise() -- in front of any other surface in band 5
toolbar:z(nil) -- back to band 0, behind the sibling it followed
| Rule |
Detail |
| The bands |
0 is every widget’s own. A window’s frame stands at -100 behind its content, an item’s contents shown on hover at 90, the item in hand at 100. A band from -9 to 9 moves a widget among its siblings without crossing any of those. |
| Popups stay in front |
A list menu, a dropdown’s list and the kin window’s menu open at band 10, over every band you name, and take the click they show. The item in hand stays over them. |
| The restore |
:z(nil), widget:revert(), :reload and disable give back the band and the place behind the sibling it followed. A widget that was the first of its band goes back first within it. A sibling that has left, or now stands in a higher band, leaves the widget at the front of its band. A band the server or the client wrote over yours stands. |
| Two addons |
Each may name a band on one widget. The latest named wins on screen, and each nil drops its own. The band given back is the one the client had. |
| A surface of yours |
The same verbs and the same level. :z(nil) gives back the band and the place it was built in. A surface directly in the addon layer takes a band over the other addons’ surfaces. |
| Refused |
Each raises, naming why. A tree’s root has no siblings. A widget the client stands directly on a character’s root (the HUD, the login screen, a popup) is the screen itself, beside which stand only the popups and the item in hand. A column’s or row’s child is placed in its order. A region is painted in its painter’s order. :z(n) also raises outside -9..9, on a number that is not whole or not a number, and with a second argument. |
Letting the user drag it (unprotected)
The client gives the drag gesture to windows alone, by their caption. widget:draggable(h) gives it to any widget, by a handle the user presses.
| Method |
Returns |
Permission |
Description |
widget:draggable(h) |
self |
Unprotected |
Arms it: pressing h drags widget. Arming again changes the handle. |
widget:draggable() |
Widget \| nil |
Unprotected |
The handle your addon armed, the same object you passed. Never another addon’s. |
widget:draggable(nil) |
self |
Unprotected |
Drops your binding. |
local chat = hafen.session():current():ui():match("@ChatUI")
local grip = hafen.ui():image():source(hafen.asset():get("grip.png")):parent(chat)
chat:draggable(grip) -- pressing the grip drags the chat
chat:draggable() -- the grip back
chat:draggable(nil) -- the chat stops being draggable
| Rule |
Detail |
| The handle is a widget |
The target itself drags the whole thing. A grip adopted into it with :parent(w) drags from there alone. A button of yours anywhere works too. A window refuses itself as its handle (its caption already drags it). Any other handle on a window is accepted. |
A drag writes your :position level |
:position() reads where the user dropped it. :position(nil), :reload and disable put the stock place back. The client’s own store still records what the user placed. On a widget directly on the HUD or in the layer the level is the user’s place, so it follows a resize of the screen (Re-layout). Your own :position(x, y) over it follows the same way, from where it lands. |
| The clamp |
At least 100 design pixels of the widget, or the whole of it when smaller, stays inside its parent. That is the client’s own rule for its windows. |
| The gesture holds the pointer |
From press to release, off-window included. Nothing underneath is clicked, and the widget follows a pointer that outruns the handle. |
| The press belongs to the drag |
A widget armed as a handle stops answering clicks of its own while the binding stands. Give a widget with clicks a grip of its own. |
| Re-layout |
Resizing the game window re-places the client’s panels, and a window directly on the HUD or in the layer keeps its place relative to the screen: its centre stays at the same fraction of the screen’s width and height. A window within 10 design pixels of an edge, or left partly past it, stays on that edge, and a window that fits is always kept whole on screen. A client window follows. A window your addon built follows once something has placed it: your :position(x, y), or the user dragging it by its title. Any level on a widget directly on the HUD or in the layer follows too, unless the widget covers the screen, whoever placed it: your :position(x, y), a :draggable drag, the title-bar drag of a window your level holds, a place :remember put back. A rule’s position keeps its pixels, since a rule says a place that follows the screen with an anchor, and :position(nil) lands a client window at the user’s relative place. Anything you anchored to a window that moved follows it on the next step. |
| Two addons |
May arm one widget: one drag moves it once, both levels take the landing, each nil drops its own. Both Dragged handlers fire. |
| Endings |
The arming ends when the target or the handle leaves the tree. A grip pressing three targets keeps pressing the other two. widget:revert() drops it too. A handle that has left the tree arms nothing and chains, with :draggable() reading nil. A non-widget raises. A stale target is a no-op. |
Knowing when one was dragged
widget:on("Dragged", fn) fires once, on release, with event:x()/event:y() where the widget landed — the numbers :position() reads in that frame, clamp included. Not cancelable. Does not fire for your own :position(x, y). A press that never moved says nothing.
chat:on("Dragged", function(drag_event)
hafen.log():write("chat dropped at " .. drag_event:x() .. ", " .. drag_event:y())
end)
Letting the user resize it (unprotected)
The client gives the resize gesture to one window in the game, the map, by its corner grip. widget:resizable(h) gives it to any widget by a handle of yours. :resizable(true) gives a window of yours the client’s own grip.
| Method |
Returns |
Permission |
Description |
widget:resizable(h) |
self |
Unprotected |
Arms it: pressing h resizes widget. Arming again changes the handle. |
widget:resizable(true) |
self |
Unprotected |
On a window you built: the client’s own corner grip, drawn by the frame. |
widget:resizable(false) |
self |
Unprotected |
The client’s grip off again. |
widget: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. |
widget:resizable(nil) |
self |
Unprotected |
Drops your handle binding. |
local corner = hafen.ui():image():source(hafen.asset():get("corner.png")):parent(inventory)
inventory:resizable(corner) -- pressing the corner resizes the window
inventory:resizable() -- the corner back
inventory:resizable(nil) -- the window stops being resizable
| Rule |
Detail |
| The top-left stays put |
A resize moves no origin, the client’s own rule. The widget grows away from its corner. |
| The floor |
One design pixel each way. |
A resize writes your :size level |
:size() reads the box it landed at — a window’s content box, the value the pointer drives. :size(nil), :reload and disable put the stock box back. On a window that packs itself around its contents the gesture is inert. The box is undone as fast as the pointer writes it, and nothing raises. |
| Handles |
The press, the pointer and the re-layout behave as a drag’s. A window is accepted as its own resize handle (nothing in the client’s chrome resizes it from the frame). Where the client’s own sizer is live, both gestures run and the last write wins. Two addons may arm one widget. |
| The client’s grip |
A window your addon built alone. A bare widget has no frame to draw it on, and a client window keeps its own chrome, so both refuse naming :resizable(h). What the grip drives is the same content box, so Resized, :remember and a size level answer the same. |
| Endings |
As a drag’s: the target or handle leaving the tree ends the arming, widget:revert() drops both bindings, a stale handle arms nothing, a non-widget raises. |
Knowing when one was resized
widget:on("Resized", fn) fires once, on release, from a handle of yours and from the client’s grip alike. event:w()/event:h() is the box the widget landed at, the numbers :size() reads in that frame. Not cancelable. Does not fire for your own :size(w, h). A press that never moved says nothing. Both addons hear it when two armed one widget.
inventory:on("Resized", function(resize_event)
hafen.log():write("window is now " .. resize_event:w() .. " by " .. resize_event:h())
end)
Remembering where the user put it (unprotected)
widget:remember(name) keeps a widget’s place and box under a name of yours. It puts back what the name holds the moment you call it, and saves where the widget stands from then on. widget:remember(name, store) keeps them in the rows the store names: hafen.store() for one place every character shares, session:store() for that character’s. No handler of yours, nothing in your manifest.
| Method |
Returns |
Permission |
Description |
widget:remember(name) |
self |
Unprotected |
Remembers it, and puts back what that name holds. |
widget:remember(name, store) |
self |
Unprotected |
The same, in the rows store names: hafen.store() shared by every character, session:store() that character’s. |
widget:remember() |
string \| nil |
Unprotected |
The name your addon remembers it under. |
widget:remember(nil) |
self |
Unprotected |
Stops remembering it and deletes what was saved. |
hafen.event():on("SessionEnteredWorld", function()
local chat = hafen.session():current():ui():match("@ChatUI")
chat:draggable(hafen.ui():image():source(hafen.asset():get("grip.png")):parent(chat))
chat:remember("chat") -- back where it was, and saved again after every drag
local hud = hafen.session():current():ui():match("@GameUI")
local panel = hafen.ui():widget():name("panel"):parent(hud):size(200, 120)
panel:draggable(panel)
panel:remember("panel", hafen.store()) -- one place for every character
end)
| Rule |
Detail |
| It applies on the call |
The only correct moment to put a place back is the moment you name it. What it writes is your :position and :size levels, exactly as the verbs write them. A :position(x, y) written after it wins. :position(nil) still gives the stock place back. |
| What is saved |
Where your levels stand, written to the client’s own file, never your store. It is written when a gesture lands, when the screen changes and when the widget goes. A place directly on the HUD or in the layer is saved relative to the screen, the fraction Re-layout describes, so a name saved at one window size or interface scale puts the widget back at the same relative place at another, following the screen from then on. A place inside another window, and the place of a widget covering the screen, is saved in pixels. A row saved as a fraction is put back as one. A window you built is saved where it stands and at the box it has. Your own writes or the user’s title bar and corner may have put it there. The one box not saved is a packed surface’s. A client widget’s box is saved only where you named a size. |
| What a store saves |
Only what moved after the call: the user’s drag or resize as it lands, a :position or :size of yours written after it, and a client window dragged by its own title bar off a place your levels gave it. A place nobody chose, a rule’s or the stock one, is not saved and stays the default. |
| Whose row |
A widget standing in a session’s tree, its own window or one of yours you put there, is filed under that character, like a character’s var. One standing in your layer under your addon’s own scope. A store names the row instead, wherever the widget stands. Called before that session is in the world, remember(name) has nothing to put back, says so in the log, and remembers the name anyway. |
| Which store |
hafen.store() or a session:store() of your addon. A session:store() whose session has ended, or has not reached the world, raises. Any other second argument is ignored, and the widget is remembered as with remember(name). |
| One place for every character |
remember(name, hafen.store()) on each character’s copy of a widget puts each at the one saved place. A drop on one saves it and moves no other copy: call remember again from SessionSelected to put the copy of the character taking the screen there. |
| One name, one widget |
A second widget under a name your addon holds raises. With a store, one per session: each character’s copy takes the name, and a second widget in the same session raises. Renaming a remembered widget is accepted, and so is naming another store. A name that is not a string raises. A stale widget is a no-op. |
| Dropping is not forgetting |
widget:revert(), :reload and disable drop the binding and leave the record. remember(nil) is the one thing that deletes it, from the rows it was remembered in. |
| Method |
Returns |
Permission |
Description |
widget:visible() |
boolean |
Unprotected |
Whether it is drawn. |
widget:visible(false) |
self |
Unprotected |
Hides it and records the restore. |
widget:visible(true) |
self |
Unprotected |
Gives it back yourself and drops the record. |
| Rule |
Detail |
| The restore |
On disable or :reload the widget ends up as the user was seeing it. It is visible exactly when whatever you put in its place was on screen. Hidden with nothing put there, it stays hidden, and its toggle reopens it. A relog skips the restore. |
| A widget with no toggle |
The action bar, a HUD panel, the chat: teardown leaves it hidden with nothing to bring it back. Put it back from Disable, which fires before the teardown: keep the handles and :visible(true) each. |
| One widget, one owner |
A widget another addon hid refuses, naming that addon. replace logs the same collision instead of raising, since it runs on the client’s placement path. A radial menu refuses both writes, naming s:flowermenu():visible(b). |
| Live while hidden |
A hidden server widget stays bound to its id, receives updates and fills with items, so a hidden grid still reads. |
Hiding a native window takes its toggle
Hiding a window the client itself can open (inventory, equipment, character sheet, kin, options, map, action search) takes its toggle. The key and the menu button stop reopening it, and the button’s tick goes off. Otherwise the keybinding, which fires the menu button’s click, would flip the window straight back on.
session:ui():inventory():parent():visible(false) -- the window around the grid: Tab opens nothing
session:ui():inventory():visible(false) -- the grid alone: the window and Tab stay the client's
| Rule |
Detail |
| What you own is what you point at |
The toggle belongs to the window. Hiding a widget inside it leaves the window and its key stock. |
| Swallowed, then handed back |
The key does nothing while nothing stands in for the window. :visible(true), disable and :reload hand it back. That is the escape from a :visible(false) typed at the :lua console: :reload, not a relog. |
With widget:replace(view) |
The toggle opens and closes your view, and the tick reads your view’s visibility. Nothing to wire. |
| Per window |
Hiding the inventory leaves equipment, kin, options and the map stock. There is no verb: ownership follows the hide. |
Taking one into a surface of your own (unprotected)
widget:parent(p) makes a client widget hang under a surface of yours. widget:parent(nil) gives it back whole. It is the building-time :parent(w) of a control of yours, from the other side.
local hud = session:ui():match("@GameUI")
local minimap = session:ui():match("@CornerMap") -- the client's own
local frame = hafen.ui():widget():parent(hud) -- a surface of yours, in that tree
:size(minimap:size().w + 16, minimap:size().h + 16):position(300, 200)
frame:stock{bg = {color = {43, 51, 44, 127}}, border = {box = "gfx/hud/wnd", mode = "tile"}}
minimap:parent(frame) -- the map is now inside it
minimap:position(8, 8)
-- later: minimap:parent(nil)
| Method |
Returns |
Permission |
Description |
widget:parent() |
Widget \| nil |
Unprotected |
What it hangs under. nil at a root. |
widget:parent(p) |
self |
Unprotected |
Takes it into p, a surface your addon built in that character’s tree. It lands at 0, 0. :position(x, y) places it from there. |
widget:parent(nil) |
self |
Unprotected |
Gives back the parent, the sibling order, the place and the box the client had. The layout goes first, then the widget, so a parent that packs around its children is not left fitted to your size. |
| Rule |
Detail |
| A move, not a copy |
The widget stays the client’s: it ticks, draws itself, answers its clicks and tooltips, and a bound one keeps filling. This is the reach for a surface whose value is its picture — the minimap, the portrait, a meter’s fill — which replace cannot redraw. To show that picture somewhere the widget cannot go, a mirror draws it again and moves nothing. |
| Painting order |
Your surface’s bg is a field under the client’s widget and its border a frame over both — naming and dressing. Your surface does not resize around it. |
| The surface must be in that character’s tree |
A client widget reads the login behind it. The addon layer has none, so a widget taken there would go dark. Build the surface into the HUD (hafen.ui():widget():parent(session:ui():match("@GameUI"))). The refusal says so. A mirror in the layer shows the widget’s picture instead. |
| One widget hangs in one place |
A widget another addon holds is refused naming it, and so is one standing in the 3D world. hafen.virtual():widget():add refuses one you hold here. |
| A destination that has left the tree |
Moves nothing: the client’s widget stays where it is, the call chains, nothing is recorded. A non-widget raises, naming the two surface builders. |
| The parent it left keeps its box |
A parent that packs around its children is not told a child walked out. The room stays reserved until the client adds the next one. |
| The client rebuilds its widgets |
The corner minimap is re-made when the map file changes, and a rebuilt widget never left home. Take it with session:ui():on(sel, "Added", fn) rather than once at login. |
| Destroying your surface |
widget:destroy() on a surface holding a client widget sends that widget home before anything is disposed. |
The client reuses its windows
Everything you hold on a client widget is held on that widget, not on what it currently shows. The frame one container came in is the frame the next gets. A place, a size or a caption you wrote is still there when it returns as something else. Watch for the widget rather than holding it. Dropping a level you do not hold is a no-op.
hafen.session():current():ui():on("window", "Added", function(reused_window)
reused_window:title(nil) -- whatever this frame was last used for, it is not that any more
end)
See Also
- Writes — which writes answer on which widget.
- Edit — changing what a client control does.
- Replace — hiding a whole window and standing yours in its place.
- Geometry — the same placement as a rule.
- Selectors — naming the widget you are about to move.
- Chrome —
padding, the other property that moves a window.
- Pixels — the unit of
:position(x, y) and :size(w, h).