Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Surfaces

A surface is a top-level Wayland object that holds one node tree. shell.lua returns one surface or a list of them, and every other node lives under some surface’s child. This page holds the rules all four roles share; each role has its own page.

You are buildingRoleProtocolInstances
Bar, dock, wallpaper, OSD, launcher overlay, notification stackpanelzwlr_layer_surface_v1One per matched output, id id@output; one with the bare id id for output = "active"
Settings window, dialog the user can move, tile or closewindowxdg_toplevelOne, id id
Dropdown, context menu, tooltip hanging off a panel or windowpopupxdg_popupOne, id id
Lock screenlockext_session_lock_surface_v1One per output, id id@output

An instance is one mapped copy of a declared surface; its id keys the retained scene and names the surface in mantle log (glossary).

A 32 px bar on every output that pushes windows down by its height and prints the output’s connector name:

local bar = panel {
    id = "bar",
    layer = "top",
    anchor = { top = true, left = true, right = true },
    height = 32,
    exclusive_zone = true,
    child = function(output)
        return row {
            width = "fill",
            height = "fill",
            padding = { left = 8, right = 8 },
            background = "#1e1e2e",
            children = {
                rect {
                    align_v = "center",
                    padding = { left = 10, right = 10, top = 3, bottom = 3 },
                    radius = 10,
                    background = "#313244",
                    children = { text { content = output, font_weight = 700, foreground = "#89b4fa" } },
                },
            },
        }
    end,
}

return { bar }

Properties every role takes

Every surface takes id (required), one child node and, except lock, visible; each role’s page has their table. The root is itself a box node, so it also takes the common and box node properties, except those that place a node in a parent (below): its own background, radius, padding, border_*, paint and animate. Any other key is refused, and the error names the closest accepted one or, with none close, lists them all.

The surface root

The root has no parent, so a few node properties mean something else on it:

PropertyOn a surface root
width, heightRole-specific: the layer surface’s size on a panel, the root’s size inside the configured window on a window, the popup’s size on a popup. Refused on a lock
min_width, max_width, min_height, max_heightBound the root’s size: they cap a content-sized or spanned panel, a popup, and a window or lock root
marginA panel’s offset from its anchored edges. Refused on the other roles
align_h, align_vRefused: the root sits at the surface’s origin
Everything elseAs on any box node

Reload and structural fields

The returned list is re-read on every reload. Each surface is matched to the last evaluation by its fingerprint, the fields that the protocol fixes when the object is created.

RuleBehaviour
Return valueOne surface, a list of them, {} or nothing. Any other top-level node is refused
MatchingA surface whose fingerprint is unchanged keeps its Wayland objects; a missing one is destroyed; a new one is created
Fingerprintpanel: id, layer, anchor, output, namespace. window, popup, lock: id alone. A changed fingerprint destroys and recreates that surface’s objects under the same instance ids
Structural fieldsid, a panel’s layer, anchor, output, namespace and a popup’s parent refuse a signal: they are read once per evaluation. Change them by editing the file
Live fieldsEverything else takes a signal and updates the existing object in place
Invalid live valueA signal that resolves to a bad value logs a warning and keeps the last applied spec
HotplugAn output change re-evaluates the config and adds or removes panel and lock instances; instances on other outputs keep their objects
CountAny number of panels, windows and popups; at most one lock

Per-output child

child = function(output) is for panel and lock, the roles with one instance per output. It runs with that instance’s connector name ("DP-1") on the first pass, and again only when the surface root resolves again: a signal the function or the root’s own properties read is written, or a reload (what a node reads again). Bind signals to properties inside it rather than reading them with :get(), and key per-output state by name: state("wallpaper_" .. output, ...). Returning nil leaves that output’s instance empty. A window, popup or output = "active" panel has no output name and refuses a function child. Example: per-output wallpaper.

Reset on close

Named state keeps its value while a surface is hidden, so a reopened menu shows what it showed last. List the state a panel, window or popup owns in reset_on_close to start it fresh instead: when the surface stops being shown, each state goes back to its declared initial and each scroll to the top.

local open = state("picker_open", true)
local query = state("picker_query", "")
local list_scroll = scroll("picker_list")

return panel {
  id = "picker",
  layer = "overlay",
  visible = open,
  reset_on_close = { query, list_scroll },
  child = textfield { width = 300, height = 32, on_change = function(text) query:set(text) end },
}
RuleDetail
Takesstate(...) and scroll(...) handles. A map, a capability, another signal kind or a plain value fails the evaluation, naming the surface and the entry
Closesvisible turns false, a reload removes the surface, its last output goes away, or (a popup) its parent closes
Does not closeA surface never shown, a failed reload, a reload that rebuilds it under the same id, being covered, losing the keyboard
Per-output panelResets when its last instance closes
Resets toThe initial the evaluation on screen declares, the value mantle toggle returns to; a scroll to offset 0
TimingThe turn the surface is unmapped, after it is gone, so the closed surface never paints the reset value
WriteAn ordinary write: readers re-resolve, and a state that changed runs its on_change. A state listed by several surfaces resets when any of them closes

A textfield’s draft is the engine’s and is not state; it follows its own lifetime.

Input region

A surface takes pointer input only where its content is solid; everywhere else clicks, hover and focus-follows-mouse pass through to what is below. The engine rebuilds this region on every pass.

Node under the rootClaims input
A box (rect, row, column) with a background or a non-zero border_widthIts whole box, painted bounds under its own transform; a rotated or skewed box claims its rotated shape. #00000000 counts
text, icon, image, capture, textfieldIts box, rotated with it
Any node with on_click, on_double_click, on_drag, on_wheel or submits_secure = trueIts box, even with nothing painted
A shaderNothing; its alpha is unknown to the engine. Give it a handler for a hit area
A path with a fill, or a stroke with stroke_width above 0, and at least one L, Q or C commandIts whole box, not the drawn shape. Otherwise nothing; give it a handler for a hit area
A box’s mask childNothing, nor its subtree
A transparent containerNothing; its children are asked instead
The surface root itselfNothing, even with a background; with a handler, the whole surface
Anything on a layer = "background" panelOnly a node with a handler

A claiming box that clips its children ends the walk there; one that does not also claims the descendants sticking out of it, each cut to any clipping ancestor. visible = false subtrees claim nothing. To make an empty area catch clicks, put a rect { width = "fill", height = "fill", on_click = ... } there (click outside to close).

How do I…

TaskAnswer
Pick a roleThe table at the top
Put a bar on every monitorThe example at the top
Show and hide a surfaceBind visible to named state, then mantle toggle <name>
Start a surface fresh each time it opensReset on close
Keep different state per monitorPer-output child
See which surfaces a config declaresmantle check -c <dir> prints each role and id (CLI)
Let clicks through the empty part of a surfaceNothing to do; see input region
Make a transparent area catch clicksA full-size rect with on_click (input region)
Change a panel’s layer or anchors at run timeDeclare two panels and toggle their visible, or edit the file

Gotchas

TrapFix
layer = state(...) or a signal anchor is refusedStructural fields take literals; switch between two declared panels, or edit the file
A click on a panel’s or window’s background reaches the window behind itThe root’s own background claims no input. Put the background on a child that fills the root; a panel’s omitted size fills the root on axes anchored at both edges
two surfaces declare an idSurface ids are unique across every role; rename one
A function child on a window or popup is refusedOnly panel (not output = "active") and lock have an output to pass

See also: nodes, paint, input, signals, runtime, CLI.

Source: surface parsing, accepted properties, fingerprints, instances, function child and root size, input region, reload and hotplug.