A column lays its children out top to bottom and a row left to right. They go in tree order, a gap apart, the cascade’s padding in from the edge, each child’s margin around it. Its box is what they take.
local settings_column = hafen.ui():column():gap(4):position(40, 60)
hafen.ui():label():parent(settings_column):text("Harvest")
hafen.ui():check():parent(settings_column):text("Only ripe")
hafen.ui():button():parent(settings_column):size(120):text("Go")
settings_column:size() -- as wide as the widest child, as tall as the three plus two gaps
| Method | Returns | Permission | Description |
|---|---|---|---|
hafen.ui():column() |
Widget |
Unprotected | A surface that places its children top to bottom. |
hafen.ui():row() |
Widget |
Unprotected | The same surface, placing them left to right. |
| Fact | Detail |
|---|---|
| It is a bare surface with an axis | :parent(w), :position(x, y), :name(s), :stock(t), Draw and the other surface keys answer unchanged. A column can paint a background under its rows. |
:type(), :role() |
"AddonWidget", like every bare surface. "column" or "row", which are selector roles: ["column"] is a tree key styling every column. |
| A child is anything you built | A control, a bare widget, a scroll, another column: :parent(column) while it is being built puts it in the next slot. |
| Refused children | A window: the user places it, and its chrome measures itself without telling the column. A client widget: its place is the client’s, so take it into a bare widget of yours and put that in the column. Both refusals say why. |
| Method | Returns | Permission | Description |
|---|---|---|---|
column:gap(n) |
self |
Unprotected | The room between two children, in design pixels. 0 from birth. Re-lays before it returns. |
column:gap() |
number \| nil |
Unprotected | Reads it. nil on a widget that is not a column or a row, where the write refuses naming the two builders. |
| Rule | Detail |
|---|---|
| Along the axis | A column writes y and keeps every child’s x at the left edge. A row writes x and keeps y at the top. The first child sits at the padding the column resolves to (0 until a rule, a :stock or widget:rule() gives it one). Each next child sits :gap() further on, inside its own margin. |
| Across the axis | A child sits at its own width at the start edge. No alignment, no stretch. |
| When it re-lays | On the events that change it, before the call returns. A child entering, resizing, hiding, showing or leaving. A :gap, :size or padding written on the column. A margin written on a child. Never per frame. |
| A hidden child | Takes no room. The rest close up. Showing it gives the slot back. A destroyed child leaves the same way. |
A child’s own :size(w, h) |
Honoured — the column never resizes a child — and moves everything after it. |
local labels = hafen.ui():column():gap(6)
local first = hafen.ui():label():parent(labels):text("one")
local second = hafen.ui():label():parent(labels):text("two")
assert(second:position().y == first:size().h + 6) -- placed by the column
first:text("a much longer first line")
assert(labels:size().w == first:size().w) -- re-laid on this same line
A column is as wide as its widest visible child and as tall as all of them, gaps, padding and margins included. An empty one is 0 by 0.
| Method | Returns | Permission | Description |
|---|---|---|---|
column:size() |
{w=, h=} |
Unprotected | Reads the box. |
column:size(w) |
self |
Unprotected | Pins the width. The height follows the children. The one-number arity a control’s art earns it means the same here. |
column:size(w, h) |
self |
Unprotected | Pins both. Children past the box are clipped. |
column:size(nil) |
self |
Unprotected | Both follow the content again. |
column:pack() |
— | Unprotected | Refused: a column is packed by construction. A window around a column still packs — window:pack() sizes the window to the column and follows it from then on. |
A rule’s size on the column is inert, as on every widget that owns its size. Its position and anchor are honoured.
padding is the room between the column’s edge and its children. The first child starts at the left and top inset. The box ends one right and bottom inset after the last. It is the same padding a window frame keeps, resolved by the cascade. That is a tree rule naming the column, its own :stock beneath every rule, or widget:rule() above them.
local panel = hafen.ui():column():name("panel")
panel:stock{ bg = {color = {0, 0, 0, 120}}, padding = 8 } -- the default look, 8 px inside the edge
hafen.ui():label():parent(panel):text("first") -- sits at 8, 8
margin is the room the column keeps around one child: four insets outside that child’s box. It is a property of the same cascade said about the child, by a tree rule naming it, its own :stock, or widget:rule() on it. Geometry is the property’s page.
local boxes = hafen.ui():column():gap(4)
local first = hafen.ui():widget():parent(boxes):size(40, 20)
local second = hafen.ui():widget():parent(boxes):size(40, 20)
second:rule():margin(16, 2, 0, 3) -- left, top, right, bottom, in design pixels
second:position() -- {x = 16, y = 20 + 4 + 2}
| Rule | Detail |
|---|---|
| Added to the gap, never collapsed | The child sits its left and top inset further in. The next child starts after its bottom inset and the gap. Two margins meeting across a gap are both kept. :position() on a child is that arithmetic. The sum is made in the client’s own pixels. A total added up from controls’ :size() reads can differ from the read by one on a scaled client. Bare widgets you sized add up exactly. |
| Across the axis | The column is as wide as its widest child with that child’s left and right insets. A right inset is room the box keeps. A left one is an indent for that row alone. |
| Whose margin it is | A rule naming a child gives that child room. A rule naming the column gives the column room in what it stands in. A default for the rows is a :stock each declares, under any theme’s rule. |
| Outside a column or row | Moves nothing. The property still resolves and widget:style() reads it. |
| Rule | Detail |
|---|---|
:position(x, y) and :position(nil) on a child |
Refused, naming this. Change the order instead: :parent(other) while it is being built takes it out, :destroy() at any time, or change the gap. |
A rule’s position or anchor on a child |
Inert: it installs, matches and moves nothing. widget:style() still reports it. margin is the one layout property a rule writes on a child. |
:position() on a child |
Reads the slot the column chose, in design pixels within the column. |
Nesting covers the rest. A row inside a column is an icon and a checkbox on one line. A column inside a column with a left padding is an indent. A column inside a scroll scrolls once it outgrows the box. An inner column that grows re-lays the outer one in the same call.
A window packed around a column. In it a row (icon and checkbox) and a switch. Then the group the switch governs, indented by a left padding and disabled until the switch is ticked. Then a scroll holding a column of rows.
local harvest_window = hafen.ui():window():title("Harvest"):position(80, 120)
local panel = hafen.ui():column():gap(4):parent(harvest_window):position(0, 0)
local header_row = hafen.ui():row():gap(4):parent(panel)
hafen.ui():image():source("gfx/hud/chr/farming"):size(24, 24):parent(header_row)
hafen.ui():check():parent(header_row):text("Only ripe")
local advanced_switch = hafen.ui():check():parent(panel):text("Advanced")
local group = hafen.ui():column():gap(4):parent(panel)
group:stock{ padding = {16, 0, 0, 0} } -- a left padding is an indent
hafen.ui():check():parent(group):text("Also unripe")
hafen.ui():entry():parent(group):size(120)
group:enabled(false)
advanced_switch:on("Changed", function(checked) group:enabled(checked) end)
local scroll_box = hafen.ui():scroll():parent(panel):size(160, 120)
local list = hafen.ui():column():parent(scroll_box):position(0, 0)
for index = 1, 30 do hafen.ui():label():parent(list):text("row " .. index) end
harvest_window:pack() -- the window is exactly the panel
| Part | What it relies on |
|---|---|
| The row | Both children at y = 0, the checkbox :gap() after the icon’s :size(24, 24) box. |
| The indent | The group’s padding: its first child sits 16 further right and the group is 16 wider than its rows. A theme naming the group can change it. A margin on the group would move the group itself. |
| The switch | Stands outside the group, so group:enabled(false) reaches the rows and not the switch. Each row’s own :enabled() stays true. |
| The scroll’s bar | One of scroll_box:children(). Its :range() follows the column inside: max is 0 while it is empty and moves with every row. |
| The window | Packed once, it follows the panel: a row added after :pack() grows the window before the call returns. |
Only two :position writes |
The panel’s inside the window and the list’s inside the scroll — the two parents that do not lay children out. |
The same panel mounts on your addon’s page of Options ▸ AddOns. opts:panel(fn) hands a root that is already the column (root:gap(4) stands where the panel line does). There is no window to pack. The page’s box scrolls what outgrows it. A control showing a setting is bound to its option. The page is rebuilt on every visit.