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
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 = true,
    width = "Fill",
    child = function(output)
        return row {
            width = "Fill",
            height = "Fill",
            padding = { left = 12, right = 12 },
            background = "#1e1e2e",
            children = { text { content = output, foreground = "#cdd6f4", align_v = "Center" } },
        }
    end,
}

return { bar }

Properties every role takes

Every surface takes id (required) and one child node. The root is itself a box node, so it also takes the common and box node properties: 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.

PropertyValuesDefaultBehaviour
idStringRequiredThe surface’s identity across reloads and the prefix of its instance ids. Structural. Unique across every role; a duplicate is refused
childOne node; function(output) on a panel or lock (per-output child)NoneThe root’s one child. nil leaves the surface empty
visibleBoolean or signaltrueCreates and destroys the protocol object, not a hidden map. Retained state and id survive. lock refuses it

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 measured size, so they cap a content-sized panel or popup
marginA panel’s offset from its anchored edges. Ignored on the other roles
align_h, align_vIgnored: 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, monitor, 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, monitor, 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 monitor = "Active" panel has no output name and refuses a function child. Example: per-output wallpaper.

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, button) with a background or a non-zero border_widthIts whole box, painted bounds under its own transform. #00000000 counts
text, icon, image, capture, textfieldIts box
A button with on_click, on_drag, on_wheel or submit = trueIts box, even with nothing painted
A shaderNothing; its alpha is unknown to the engine. Put a button over it for a hit area
A transparent containerNothing; its children are asked instead
The surface root itselfNothing, even with a background
Anything on a layer = "Background" panelOnly such a button

A claiming box that clips its children ends the walk there; visible = false subtrees claim nothing. To make an empty area catch clicks, put a button { 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>
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 button 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 width = "Fill", height = "Fill" child; on a panel, make the panel "Fill" on those axes too
two surfaces declare an idSurface ids are unique across every role; rename one
margin or align_h on a window or popup root does nothingSet it on the child
A function child on a window or popup is refusedOnly panel (not monitor = "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.