LavaUI docs

interface Compositor

Control plane

interface Compositor

Not documented.

idl/lava.npidl:919

Methods

void ActivateWindow(surfaceId: u32) raises(SurfaceNotFound)

Brings a window forward: restores it if minimized, raises it, focuses it.

What a dock icon does when it is clicked, and the counterpart to Minimize — which until now put windows somewhere nothing could reach.

Deliberately three things in one call rather than three calls a shell sequences itself. "Activate" is a single intention, and a shell that had to spell it out could spell it out differently — restoring without raising, focusing without restoring — leaving windows in states no gesture in the desktop produces.

A window on another workspace is not followed by switching to it: that would move the user somewhere they did not ask to go. It is restored and raised on its own workspace, where they will find it when they arrive.

idl/lava.npidl:1324

void BeginMove(surfaceId: u32) raises(SurfaceNotFound)

"The user has grabbed this window; take the pointer and move it."

The client-side decoration counterpart of pressing a title bar, and it carries no coordinates for the same reason ScrollUnclaimed does not: the compositor still has the pointer that produced the press, and a position from the client would be a position it could aim anywhere.

The move ends when the button comes up, which the compositor sees and the client does not — from the moment this returns, the pointer is the compositor's and the client stops receiving motion. The release it would otherwise never see is delivered anyway, synthetically, before the grab starts: a client left believing a button is still down is a client with a stuck drag, and that is a worse bug than a release that arrives early.

Ignored, deliberately, when no button is down. Without that check a client could call this at any time and have the window follow the cursor until the next click, which is not a window manager anybody asked for.

idl/lava.npidl:1167

string CaptureScreen(surfaceId: u32, includeSelf: boolean, x: i32, y: i32, w: i32, h: i32, maxSide: i32) raises(SurfaceNotFound, CaptureFailed)

The whole screen, written to a file, and the path to it.

A path and not the bytes, which is the one design decision in this call. A shared-memory reply is capped at 512 KiB — a screenful of PNG is past that at 1280x720 and nowhere near it at 4K — and a picture that sometimes fits is worse than one that never does. Both processes are on the same machine and the same filesystem, so the file is the transfer, and it costs one write and one read instead of a copy through a ring.

It also makes the ordinary case free: the client hands the path straight back to RegisterImage, and the pixels never leave the compositor at all.

The file is the caller's from the moment this returns — nothing deletes it, and a client that takes twenty screenshots and exits leaves twenty files in the temporary directory unless it cleans up after itself.

The output the pointer is on, with every window on it, foreign and Lava alike, and the wallpaper behind them. Composited into a buffer of its own rather than read back from the front buffer, so it costs one offscreen frame and needs nothing to be on screen at the time.

includeSelf false leaves the calling surface out of the picture, which is what a screenshot tool wants and cannot arrange for itself: by the time it can ask, its own window is already up. Its shadow and its frost plate go with it — a window that is not in the shot must not leave a soft rectangle where it was.

The crop is in framebuffer pixels and w/h <= 0 means the whole screen. Cropping here rather than in the client is not an optimisation: a Lava client has no codec, so a region of a PNG is not something it can take for itself.

maxSide > 0 downsamples so the longer encoded edge fits. A screenshot wants 0: the point of it is the pixels that were there.

idl/lava.npidl:1837

Capture CaptureSurface(surfaceId: u32, x: i32, y: i32, w: i32, h: i32, maxSide: i32) raises(SurfaceNotFound, CaptureFailed)

A PNG of what this window currently looks like.

surfaceId is any id from SubscribeWindows, not only the caller's own surface. A LavaUI window is read back from its canvas framebuffer. A foreign Wayland or X11 window is read from the buffer it last committed — shm via a CPU map, GPU clients via wlr_texture_read_pixels. That is how automation (and anything that needs a file) reads a window. The app switcher does not use this: it emits ImageSurface and the compositor imports the dma-buf in place. Pixels already live here.

Also what automation uses. A LavaUI client has no framebuffer of its own, so the one agent command that checks what the user would see has to ask the process that drew it.

Region in framebuffer pixels; w/h ≤ 0 means the whole surface, and maxSide > 0 downsamples so the longer encoded edge fits — a full 4K window is several megabytes of PNG that no assertion (and no 3D card) needed. Region crop is honoured for Lava surfaces; foreign windows currently return the whole buffer (then maxSide).

Bytes, not base64. The agent's protocol is JSON and wants text, but that is the client's protocol, not the renderer's: encoding here would put a third more bytes on the wire and a decode at the far end in service of something the compositor does not speak.

idl/lava.npidl:1799

u32 CreateMenuSurface(arenaId: string, width: u32, height: u32, appId: string) raises(ArenaNotFound)

Asks for the surface a context menu is drawn into.

A third way to create a surface, for the same reason CreatePanel was a second one: what the caller decides is different again. A window chooses its size and the compositor chooses where it goes; a panel chooses its depth and the compositor chooses the rest; a menu chooses neither. It is put where the pointer was, at the size it says it measured, and both of those arrive later — with ShowMenu — because neither is known when the process starts.

What the compositor does differently with one of these is the whole reason it is its own kind:

  • it is stacked above panels, in its own scene tree. A menu that opened under the taskbar would be a menu the top of the screen cannot use.
  • it is hidden until ShowMenu and again after the reply, so a resident menu process costs the desktop nothing between menus — no composite, and nothing in the way of a fullscreen client's direct scanout.
  • while it is up the compositor holds a pointer grab: a press anywhere else dismisses the menu and is swallowed rather than landing in whatever was underneath. That is what every other menu on the desktop does, and doing it here rather than in the client is what makes it work over a foreign window, which a client cannot hit-test.
  • it is not in the window list, has no title bar, and never appears in the switcher.

width and height are what the client expects its largest menu to need. They size the arena and nothing else — every menu is drawn at the size ShowMenu names.

One per client. Asking twice returns the surface already open.

idl/lava.npidl:1129

u32 CreatePanel(arenaId: string, edge: PanelEdge, thickness: u32, reserve: boolean, title: string, appId: string) raises(ArenaNotFound)

Asks for a panel: a surface docked to a screen edge.

A separate call rather than a flag on CreateSurface, because the arguments genuinely differ. A window is given the size it asked for and put where the compositor likes; a panel is given the length of its edge and chooses only its thickness. Passing width and height for something that controls one of them would be an interface that lies about what the caller decides.

Everything downstream is the same surface id as any window: SubscribeInput, Present, CaptureSurface and DestroySurface all work on it unchanged. That is the point of making this a different way to create a surface rather than a different kind of thing to own.

What differs is what the compositor does with it. A panel gets no title bar — there is nothing to drag, close or maximize — it is stacked above ordinary windows, and if reserve is set, windows are laid out inside what is left rather than under it. A taskbar wants that; an overlay that floats over the desktop does not.

thickness is in the direction the panel is not long: height for a top or bottom panel, width for a left or right one. Like a window's requested size it is a request — the real one arrives as the opening Resize on the input stream, which is also how a panel learns its length and how it hears about the screen being resized.

appId names the application, the way it does on CreateSurface: a panel is not in the window list, but it is how the compositor recognises a component it started when that component says it is still drawing.

idl/lava.npidl:1091

u32 CreateSurface(arenaId: string, width: u32, height: u32, title: string, frame: WindowFrame, appId: string) raises(ArenaNotFound)

Asks for a window driven by arenaId, and returns the surface id that names it in every later call.

The arena must already exist: the client creates it, the renderer attaches. That ordering is what makes the arena's capacity and geometry the producer's business alone.

width and height are a request, not a guarantee — the window manager has the last word, and the size the client should actually draw at is the one that arrives on SubscribeInput as the opening Resize. A client that trusts these numbers instead is a client that draws at the wrong size on any tiling WM.

frame says who draws the non-client area — see WindowFrame. With client the size above is the whole window rather than the part below a title bar, which is the only thing about this call it changes: a frameless surface is presented, resized, captured and destroyed exactly like a framed one. appId is what this application calls itself — the identity a dock finds a desktop file and an icon by, and the identity two windows of one application share. A title is not that: it changes with the document. Empty is allowed and means "no icon for me", which is what a client with nothing installed on the system should say rather than guessing.

idl/lava.npidl:1057

void DestroySurface(surfaceId: u32) raises(SurfaceNotFound)

Gives the surface back: the window closes and the arena is released.

Not the only way a surface ends — closing the input stream does it too, and so does the user closing the window — but it is the one a client that never subscribed to input can use.

idl/lava.npidl:1596

vector<string> DumpAtlasImages(directory: string) raises(AtlasDumpFailed)

Writes every atlas page to directory as a PNG and says where they went.

Pixels rather than numbers, because the question about an atlas is often "what is in it" — a coverage atlas 60% full of one font at nine sizes looks completely different from one packed with a UI's worth of glyphs, and no occupancy percentage tells the two apart.

Written to disk rather than returned as bytes: a page is up to a couple of hundred megabytes at the device maximum, the caller is on the same machine, and a file can be opened again tomorrow. The returned paths are what was actually written, so an empty list means nothing could be.

Costly and deliberately so — each page is copied off the GPU, which idles the device. Ask when a human asks, not on a timer.

idl/lava.npidl:2003

void EndSession()

Ends the session. Same as the compositor's own quit binding.

For the panel's Log Out item. A client cannot terminate the display itself — it does not own it — and killing its parent would also kill a panel that was started from a terminal rather than by the compositor. This is the one call that means "I am done with this desktop".

Orderly: the reference is unlinked, the shell is stopped, clients see a display that closed. There is no confirm here; the panel asks the user first.

idl/lava.npidl:2034

void ForgetWindowPoster(surfaceId: u32)

Forget the cached poster for a window, so the next ImageSurface that names it imports the buffer that window is showing now.

Posters are cached per (surface, maxSide) because a shelf of twenty windows must not re-import twenty dma-bufs on every frame. The switcher never needs to say this: it is spawned per invocation, and the cache is dropped when its overlay opens and again when it closes. A dock is the opposite kind of client — it lives for the whole session — so hovering an icon a second time would otherwise show the picture taken the first time, which is a screenshot of the past presented as a preview.

Cheap: dropping a poster costs one texture, and the next resolve is a dma-buf import on the same device, not a readback. Say it once when the preview opens rather than per frame — a poster re-imported every frame is a capture of the whole shelf at 60 Hz.

An unknown id is a no-op rather than an error: a window that closed between the hover and this call has no poster to drop, which is the answer the caller wanted.

idl/lava.npidl:1861

Appearance GetAppearance()

The desktop's look, for a client matching its own surfaces to it.

A call rather than a stream, unlike the focus next door, because of how often the answer changes: focus changes whenever the user clicks, and this changes when somebody edits a config file and sends SIGHUP. A client that read it at startup is right for the rest of the session in every case except that one, where it is stale until it restarts — worth saying out loud, and worth turning into a stream on the day a setting here can change for a better reason than a hand edit.

No surface id: this describes the desktop, and every window on it gets the same answer.

idl/lava.npidl:1387

string GetArrangement()

How the screens share the desktop: "extend" or "mirror".

Extend gives each screen its own piece of the layout. Mirror puts the same picture on every screen, aligned at the origin. The value is what is stored, not what two screens happening to sit on top of each other would look like.

idl/lava.npidl:1505

string GetClipboard(surfaceId: u32) raises(SurfaceNotFound)

The display server's text selection, or empty if it holds none.

Empty rather than raising: "nothing has been copied" is the ordinary state of a fresh session, not a failure, and a paste that finds nothing should insert nothing rather than make the caller handle an exception.

Synchronous, and worth being honest about the cost: a paste blocks the calling client until the compositor's loop answers, which on X11 can mean a round trip to whichever process owns the selection. That is one keystroke's worth of latency in the client that asked, and it is why this is not something to call per frame.

idl/lava.npidl:1887

vector<u8> GetClipboardPng(surfaceId: u32) raises(SurfaceNotFound)

PNG of the seat selection, or empty if it holds none or is not an image.

Same empty-not-error contract as GetClipboard. A Print Screen or a Flameshot crop lives here as image/png; a text selection answers empty. The bytes are the file, not a texture id — a terminal that wants to paste a screenshot writes them out and inserts the path.

idl/lava.npidl:1938

GpuReport GetGpuReport()

What is in the compositor's VRAM, and which window asked for it.

The compositor is the only process that can answer this. It renders every LavaUI surface on the desktop on one Vulkan device, so "why is the compositor holding a gigabyte" is a question about windows a client cannot see, atlases it does not own, and a texture cache it never filled.

A whole snapshot per call, like WindowList and for the same reason: it is a few tens of kilobytes, a reader that has one is correct, and allocation events are far too bursty to be worth streaming. Polling this once a second is what a debug view should do.

Not [unreliable]: the caller wants the answer, and a dropped reply looks exactly like a compositor that has stopped answering.

idl/lava.npidl:1987

KeyboardSettings GetKeyboard()

The keyboard as it is set up now.

idl/lava.npidl:1441

string GetPrimarySelection(surfaceId: u32) raises(SurfaceNotFound)

The primary selection — what middle-click pastes.

A second, separate selection that Unix has had since X11 and that no other desktop makes a setting: selecting text fills it, middle-click empties it into wherever the pointer is, and neither of those touches the clipboard. Two selections is not duplication — it is what lets you paste something you copied an hour ago into a line you are assembling from things you are selecting now.

Same shape and same costs as GetClipboard, and empty for the same reason: nothing selected is the ordinary state, not a failure.

idl/lava.npidl:1908

SystemTheme GetSystemTheme()

The system colour theme Lava windows should wear.

A name, not a palette: see SystemTheme. The running value, so a settings app that just opened is not guessing from the config file.

idl/lava.npidl:1411

Wallpaper GetWallpaper()

What the desktop is painted with now.

Reports what is up, which after a failed picture is the background that survived rather than the one that was asked for. A settings app that re-reads this after a WallpaperUnreadable therefore sees the truth on screen, and its controls go back to matching the desktop.

idl/lava.npidl:1425

[unreliable] void Heartbeat(surfaceId: u32)

"I am still drawing."

For the components the compositor starts and keeps running — the panel and the dock. It already learns when one of them dies: the process is its child, and the shared-memory session is reaped when the peer goes. A shell that is still a process and no longer a shell is the case that needs saying out loud — wedged on a lock, stuck in a call that never returns, alive in every way the operating system can see and not drawing anything. A desktop where the dock stopped responding and nothing noticed is worse than one where it crashed, because a crash gets fixed by restarting it and this does not.

Sent from the client's frame loop, which is the point. A heartbeat from a timer thread proves the process exists, which the compositor could already see; one that has to pass through the loop that draws proves the thing the compositor actually cares about.

[unreliable], for the reason Present is, and because the whole cost of this feature should be one datagram every couple of seconds: a lost beat is not a lost fact, since the next one carries the same one, and the compositor waits several before concluding anything.

Every LavaUI client sends these; the compositor keeps them only for surfaces belonging to something it started. A client is not told whether it is supervised, and should not behave differently if it is.

idl/lava.npidl:1680

vector<KeyBinding> ListKeyBindings()

Every shortcut the compositor takes for itself.

So a settings app can show them, and so a user can find out that Alt+M minimizes without reading main.cpp. Compiled in for now, hence no setter — see KeyBinding.

idl/lava.npidl:1464

vector<KeyboardLayout> ListKeyboardLayouts()

Every layout and variant this machine's xkb knows about.

From the compositor because xkb is the compositor's dependency: a client would have to guess where the rules files live, and a client on a machine where they live somewhere else would guess wrong and show an empty list rather than an error.

idl/lava.npidl:1457

vector<OutputMode> ListOutputModes(name: string) raises(OutputNotFound)

Every mode a screen can run at, best first.

Separate from ListOutputs because a screen has dozens of modes and a settings app needs them only for the one the user is looking at. Modes that match in width, height and refresh are collapsed: EDID often lists the same size twice with picture-aspect or blanking flags this message does not carry, and a client keying a list on that triple would otherwise see duplicate ids.

idl/lava.npidl:1477

vector<OutputInfo> ListOutputs()

Every screen the compositor has, in layout order.

idl/lava.npidl:1467

void Minimize(surfaceId: u32) raises(SurfaceNotFound)

Hides the window without ending it. The client keeps its surface, its arena and its input stream.

Frames published while minimized cost the compositor nothing to accept and are not drawn, so a client need not know it happened — and today it is not told, because there is nothing it could usefully do with the news that a Resize does not already say.

Getting it back is the compositor's business: ActivateWindow from a taskbar that walked ListWindows, or Alt+Shift+M, which brings back every window the workspace has hidden.

idl/lava.npidl:1217

u32 OpenMenu(surfaceId: u32, x: i32, y: i32, title: string, items: vector<MenuItem>) raises(SurfaceNotFound)

"Show this menu for me, anchored here."

Returns the serial the answer will name, or 0 when the desktop has no menu client to draw it (menu = off in [shell], or it is restarting) — the same nothing a right-click on the desktop gets, and not an error.

x and y are in surfaceId's own coordinates, not the output's. A panel does not know where it is on screen and has no way to find out; what it knows is which of its icons was clicked, and this is that number. The compositor adds the surface's origin and unconstrains from there, so a menu anchored to the last icon on a dock at the bottom-right opens upward and leftward without the client working out that it had to.

Placed below and right of the anchor where there is room, and flipped above or left of it where there is not — the same xdg_popup rule a right-click menu gets. A panel at the bottom edge therefore gets a menu that opens upward without asking for it, by anchoring to the top of whatever was clicked.

Supersedes whatever menu was up, exactly as a second right-click does. A client whose own menu is replaced still gets a MenuChoice for it, with chosen 0.

Subscribe with SubscribeMenuChoice before calling this. A menu opened by a client that is not listening would be a grab the user has to click their way out of to reach a result nobody receives, so it is refused.

idl/lava.npidl:2102

[unreliable] void Present(surfaceId: u32, serial: u32)

"A frame is committed on this surface; come and get it."

[unreliable] — no reply, no ack, no round trip. Correct rather than merely cheap: the arena's published sequence is already the authority on what is current, so this call carries no information that could be lost. Dropping one costs a frame of latency, never a frame of content, and the renderer would coalesce two of them anyway.

It exists because a shared-memory store wakes nobody. Without it the renderer has to poll, which is a busy loop pretending to be a frame clock; with it the renderer blocks until a client actually has something to show.

Naming the surface is what keeps that property once there is more than one client: the renderer redraws the window that published, instead of redrawing every window because somebody did.

serial is the newest InputEvent the app had consumed when it laid this frame out — 0 before it has consumed any. It is the same number InputAck carries and means something stricter: an ack says the app has read an event, and the app reads before it lays out, so a frame published just before an ack is still one drawn without it. Riding on Present puts the answer on the frame itself, where no ordering between two streams can get between them.

What needs it is the opening Resize: a window is held off screen until a frame arrives that was laid out at the size the compositor gave it, and this is how a frame says so. Nothing else reads it, and a client that always sends 0 is one whose windows appear on the hold's deadline rather than on their first good frame.

idl/lava.npidl:1628

u32 RegisterFont(path: string, pixelSize26_6: u32, faceIndex: u32, rasterFlags: u32) raises(FontNotFound)

Registers a face and returns the id to put in GlyphInstance.fontId.

Idempotent per face, which is not the same as per path. The compositor opens the file and keys on what it contained — see canvas::FontKey — so two clients naming the same bytes by different paths share one face and one set of atlas glyphs, and a path whose bytes have changed since the last call is correctly a different face rather than a stale hit. That second half matters more than it sounds: a compositor outlives the builds of the clients it draws, and .build/…/fonts/X.ttf is a different file after every one of them.

pixelSize26_6 is 26.6 fixed point — pixels times 64 — because that is the unit FreeType and HarfBuzz both size in, and because a f32 size is not something two processes can compare for equality and agree.

faceIndex selects a face inside a .ttc/.otc collection; 0 for a plain font file. rasterFlags is canvas::RasterFlags — hinting mode, and whether to force the autohinter. Unknown bits are refused rather than ignored, so a client asking for something this renderer does not do hears about it instead of silently getting something else.

idl/lava.npidl:950

ImageInfo RegisterImage(path: string, maxPixelSize: u32, turn: ImageTurn) raises(ImageNotFound)

Registers an image and returns the id to put in an image command, with the size it decoded to. Idempotent per (path, maxPixelSize).

maxPixelSize caps the longer edge (0 = native), and is part of the identity rather than a hint: the same file wanted for a 48pt avatar and for a 200pt hero is two textures, and aliasing them would either blur one or waste the memory of the other.

A path, not pixels — the same rule the draw list follows, for a different reason. There the objection is cost; here it is ownership. The renderer is about to hold this texture for as long as the client wants it, so it opens and decodes the file itself rather than trusting a buffer it did not produce. It also means a client needs no image codec at all, which is most of what "no GPU" was already promising.

An image the client has only in memory goes through RegisterImageData below; a client with a file should still use this one, so that two clients naming the same asset share a texture without either one having to send it. turn is the one operation a client cannot do for itself. It has no GPU and, by the paragraph above, no codec — so a client that wanted a photograph a quarter turn round would have to decode it, turn the pixels, re-encode them and send the result back through RegisterImageData: a second copy of every picture, seconds of work for a large one, and the renderer decoding it twice. The turn is a pass over a buffer that is already here, so it happens here.

idl/lava.npidl:980

ImageInfo RegisterImageData(bytes: vector<u8>, maxPixelSize: u32) raises(ImageNotFound)

Registers an image from encoded bytes — a PNG or JPEG that was downloaded, generated, or unpacked from an archive and never had a path.

Encoded, not raw pixels, and that is the cheaper choice in both directions: a 300×300 cover is ~30 KB as JPEG and 360 KB as RGBA, and the renderer already has the codec. The client still needs none.

Idempotent per (content, maxPixelSize), where the identity is a hash of the bytes the renderer computes itself. It does not take a caller's cache key, and that is deliberate: a key is a name from the client's namespace, and two clients that both call their icon "logo" would otherwise be handed each other's texture. Hashing costs a fraction of the decode it usually saves.

The trade against RegisterImage is the copy. This sends the file through the ring buffer, where a path sends a few dozen bytes and lets the renderer open it — so an asset that exists on disk should still go the other way, and this one is for the image that has nowhere to be opened from.

No turn here, unlike its sibling, and not for a reason of principle: nothing has ever wanted one. Add it when something does rather than ship a second path through the decoder that no caller exercises.

idl/lava.npidl:1007

void ReleaseImage(id: u32)

Drops the client's reference to a registered image.

By id, so the two sides never have to derive the same cache key from a path and a cap and agree on the spelling. How the renderer names it internally stays the renderer's business, which is the point of handing out an id in the first place.

Not raises: releasing something already gone is how a client that has lost track ends up correct, and making that an error would only tempt callers to skip it.

idl/lava.npidl:1020

[unreliable] void ScrollUnclaimed(surfaceId: u32, dx: f32, dy: f32)

"Nobody in my tree wanted this wheel notch — it is yours again."

The other half of a conversation that already exists. When a node under the pointer claims the wheel — a text editor, a canvas that only wants Ctrl+wheel — the renderer stands aside and forwards the notch as a Scroll event instead of scrolling anything itself. If the client then walks its own chain and every handler declines, the container around those handlers should still scroll, which is what the wheel would have done had none of them been there.

In one process that hand-back is a function call. Across two it needs this, and without it a pinned inner widget silently eats the notch and the page it sits on refuses to move — which is exactly what a client running an editor inside a scrolling page looked like.

No position: the renderer still has the pointer, and it is the same pointer that produced the Scroll this answers. Sending coordinates back would invite a client to aim a scroll somewhere the user is not.

[unreliable], for the reason Present is: the renderer owns the scroll offset, this is a nudge rather than a fact, and a dropped notch costs a few pixels of travel rather than a wrong position. It also keeps the wheel — which arrives in bursts — off the client's blocking path.

idl/lava.npidl:1653

void SetAppearance(appearance: Appearance) raises(SettingsWriteFailed)

Sets the desktop's look, now and for the next session.

Live because it can be: the radius and the shadow are numbers the renderer reads per frame, so every window on screen takes the new ones without being told anything. That is what makes a slider here worth having rather than a text field and an Apply button.

Values are clamped to what the compositor will draw, so a client does not have to know the limits — call GetAppearance afterwards to see what was actually taken.

idl/lava.npidl:1405

void SetArrangement(mode: string) raises(SettingsWriteFailed)

Sets extend or mirror, now and for the next session.

Anything other than "mirror" (and the aliases "clone" / "same") is taken as extend. Switching to extend spreads screens that were stacked; switching to mirror does not forget their extend positions, so switching back puts them where they were.

idl/lava.npidl:1513

void SetBackdropBlur(surfaceId: u32, radius: f32) raises(SurfaceNotFound)

Frost the desktop behind this surface.

The client's own pixels stay sharp — this is not .blur(). In-window backdropBlur can only sample this surface's framebuffer, so a window that wants the wallpaper and the windows under it softened has to ask the compositor, which is the only process that can see them.

radius is in layout pixels. 0 turns the effect off. The compositor draws a blurred copy of what is behind the window, then the window on top; a translucent WindowBackdrop is what lets the frost show through. An opaque fill hides it completely.

A property of the surface, not a draw command: the radius changes rarely, and the pixels it samples are not the client's to name.

idl/lava.npidl:1968

void SetBackdropBlurRegion(surfaceId: u32, radius: f32, x: f32, y: f32, w: f32, h: f32, cornerRadius: f32) raises(SurfaceNotFound)

Frost a rectangle of the desktop behind this surface, not the whole window.

SetBackdropBlur is the window-sized version: a terminal that is itself the glass wants that. A panel that is a 32pt strip on a 600pt surface does not — frosting the whole thing would haze the desktop under every pixel the panel owns, most of which are empty so clicks fall through. A menu dropdown is a small rect on that surface, and this is how it asks for frost under just that rect.

x,y,w,h are in the surface's own coordinates. w or h of 0 is the whole surface, the same as SetBackdropBlur. cornerRadius rounds the frost plate so it matches the popup sitting on it; 0 is square. radius 0 turns the effect off.

idl/lava.npidl:2019

void SetBackdropBlurRegions(surfaceId: u32, radius: f32, rects: vector<FrostRect>) raises(SurfaceNotFound)

Frost several rectangles of the desktop behind this surface at once.

A list, because one rectangle was not enough, and for the reason SetInputRegion grew one. A menu dropdown is a single rect; a dropdown with a fly-out submenu beside it is two, and neither contains the other. With one plate between them the second call simply replaced the first, so opening a submenu took the frost off the menu it came from. Their union is no answer either — the corner between them is desktop nothing is drawn over, and frosting it would leave a blurred rectangle hanging in mid-air.

Each rect gets its own plate. They may overlap. An empty list turns the effect off, the same as radius 0. One rect here is exactly SetBackdropBlurRegion, which stays for callers that only ever have one.

idl/lava.npidl:2050

void SetClipboard(surfaceId: u32, text: string) raises(SurfaceNotFound)

Puts text on the display server's selection.

Not [unreliable]: a copy that silently did not happen is discovered at the paste, in another app, long after the user has stopped believing they pressed anything. Worth a round trip.

idl/lava.npidl:1894

void SetClipboardImageFile(surfaceId: u32, path: string) raises(SurfaceNotFound)

Offers the PNG in path as the selection, as image/png.

A path for the same reason CaptureScreen answers with one: a picture does not fit in a shared-memory message, and both processes can already see the same filesystem. The compositor reads the file once, holds the bytes, and hands them to whoever pastes — so the file may be deleted the moment this returns.

Not [unreliable], for the reason SetClipboard is not: a copy that did not happen is discovered in another application, minutes later, by somebody who has stopped believing they pressed anything.

idl/lava.npidl:1929

[unreliable] void SetCursor(surfaceId: u32, shape: CursorShape)

The pointer image while the pointer is inside this surface.

A client cannot set the cursor: there is one pointer, it belongs to the seat, and the compositor is the only thing that can see what else is under it. So this is a preference stored against the surface, applied while the pointer is over that surface and dropped the moment it is not — which is what stops a client that asked for a resize arrow and then stopped answering from leaving that arrow on the whole desktop.

Sent when the pointer crosses from one view to another, which the client learns from the NodeHover event the renderer already sends it. The round trip is real but it is not in the way: the hover tint is drawn by the renderer without asking anyone, and only the cursor waits for this.

[unreliable], for the reason Present is: it is on the pointer's path, it arrives in bursts as the pointer crosses a toolbar, and a client that blocked on each one would put a shared-memory round trip between the pointer and every frame. The cost of a lost one is the wrong cursor until the pointer crosses something else, which is also what happens when a client is simply slow to answer.

idl/lava.npidl:1373

void SetFullscreen(surfaceId: u32, on: boolean) raises(SurfaceNotFound)

Fills the output with this surface, or gives it back its old geometry.

For a client whose whole purpose is to cover the screen — a screenshot tool showing a frozen desktop, a presenter, a lock screen. The compositor already does this for foreign windows that ask through xdg-shell; this is the same thing said over the control plane, which is the only way a Lava client can say anything about its own frame.

The surface is resized to the output and drawn above the panel and the dock, with no title bar and no shadow. Turning it off restores what it had before, which the compositor remembered rather than the client.

idl/lava.npidl:1951

void SetInputRegion(surfaceId: u32, rects: vector<InputRect>) raises(SurfaceNotFound)

Where a surface takes pointer input, in its own coordinates.

For a panel that draws less than it covers. A dock floating over the desktop is a full-width strip with a few icons in the middle of it, and panels are hit-tested above windows — so without this, the empty half of that strip swallows every click meant for the window underneath.

A list, because one rectangle was not enough. It was, for as long as the shapes that needed it were single runs — a dock's icon row, a menu's dropdown. Then the panel grew notifications: cards 380 pt wide at the right edge, under a 32 pt strip that spans the screen. Neither rectangle contains the other and their union is the whole top of the display, so a panel with a toast up claimed every click along the top edge and the desktop under it went dead until the notification expired. Two rectangles describe it exactly; one cannot describe it at all.

A point is inside the region if it is inside any rectangle. They may overlap, and an empty list restores the default, which is the whole surface — the answer for every window, which never calls this.

Still rectangles rather than an arbitrary region: what a shell draws is rectangles, and a list of them says everything a scanline region would without either side owning region arithmetic.

idl/lava.npidl:1349

void SetKeyboard(settings: KeyboardSettings) raises(SettingsWriteFailed)

Changes the keyboard, now and for the next session.

wlroots hands the new keymap to every connected client as a side effect, so a layout change reaches applications that are already running — which is the difference between this and editing the file, where it only reaches them on SIGHUP.

idl/lava.npidl:1449

void SetMinSize(surfaceId: u32, minWidth: u32, minHeight: u32) raises(SurfaceNotFound)

The smallest this window is willing to be, in pixels.

A window has a size below which its layout stops being a layout — a column that no longer fits its own labels, a row whose right-hand end is simply gone. Only the client knows where that point is, and until this call there was no way for it to say so: the compositor clamped every surface at one global floor meant to stop a window vanishing, not to keep anybody's design intact.

Advisory in the sense that the compositor still owns the geometry: it clamps interactive resizes to this, but a window that cannot fit on a small output is made to fit anyway. A minimum larger than the work area is not a request the compositor can honour and is not treated as one.

It bounds what the user can drag the window down to. It does not resize a window that is already smaller — one that opened at less than its own stated minimum asked for that size, and this call is not a way to argue with it.

Zero on either axis means "no opinion about that direction", which is what every existing client is saying by never calling this.

idl/lava.npidl:1190

void SetOutput(request: OutputRequest) raises(OutputNotFound, SettingsWriteFailed)

Reconfigures a screen, now and for the next session.

A rejected mode does not cost the session: the compositor falls back to the display's preferred mode rather than leaving a screen showing nothing, which is the one failure a user cannot recover from without another machine. The fallback is visible through ListOutputs, so a client can tell that what it asked for is not what it got.

idl/lava.npidl:1486

void SetPanelThickness(surfaceId: u32, thickness: u32, reserved: u32) raises(SurfaceNotFound)

Changes how deep a panel is, and how much of that depth windows are laid out around.

Two numbers because they stopped being the same one. A panel is normally both — a 32-pixel strip that reserves 32 pixels — but a panel with a menu on it has to be able to draw a dropdown, and a dropdown does not fit in the strip that owns it. So the panel grows its surface to the room the menu needs while reserving the strip it always did: the windows underneath do not move, and the open menu is drawn over them.

It also decides what the panel takes clicks in, and that falls out rather than being a second thing to set: a surface is hit-tested where it is, so a grown panel receives the click that lands outside the open menu — which is exactly the click that should close it.

reserved may not exceed thickness; it is clamped if it does. A window that was maximized against the old reservation is re-fitted to the new one, because a maximized window is a promise about the work area rather than a size someone chose.

idl/lava.npidl:1238

void SetPrimaryOutput(name: string) raises(OutputNotFound, SettingsWriteFailed)

Makes this screen the primary, now and for the next session.

The panel moves here immediately. An empty name means no preference — the screen at the layout origin, which is where the panel sat before this existed. Unplugging the primary does not forget the name: another screen stands in until it comes back. ListOutputs always reports the screen that is actually hosting the panel.

idl/lava.npidl:1496

void SetPrimarySelection(surfaceId: u32, text: string) raises(SurfaceNotFound)

Offers text as the primary selection.

Called when a selection is made, not when a copy is asked for — which is the whole convention, and the reason a client should call this from wherever a drag ends rather than from a keybinding.

idl/lava.npidl:1915

void SetSystemTheme(theme: SystemTheme) raises(SettingsWriteFailed)

Sets the system colour theme, now and for the next session.

Every subscribed client is told; a Lava window that uses Theme.current retints without restarting. Written to lava.conf as [theme] name=….

idl/lava.npidl:1417

void SetWallpaper(wallpaper: Wallpaper) raises(WallpaperUnreadable, SettingsWriteFailed)

Sets the desktop background, now and for the next session.

The picture is decoded before anything changes, so the failure case leaves the desktop exactly as it was: a mistyped path raises WallpaperUnreadable and the old background stays up and stays saved. This is the one setter here that can refuse, and it is why it can — every other setting on this interface is a number or a name that cannot be wrong in a way the compositor could detect in advance.

Written to lava.conf as a [background] block.

idl/lava.npidl:1437

void ShowMenu(surfaceId: u32, serial: u32, width: u32, height: u32) raises(SurfaceNotFound)

"I have laid out the menu for serial; it is width × height."

The client's half of opening a menu, and the reason the surface is not simply shown when the request goes out: only the client can measure the list. It has the face, the padding and the shortcut column; the compositor has the item strings and no idea what they come to in pixels.

The compositor resizes the surface, places it against the anchor the request carried, unconstrains it into the output's work area — flipping it above the pointer or left of it rather than letting it run off the screen, the same rule xdg_popup gets — takes the grab and reveals it. The reveal waits for a frame drawn at the new size, like any other window's first frame, so a menu never appears at the previous menu's dimensions.

Ignored for a serial that is not the open request. A client that measured a menu the user has already dismissed is not wrong; it is late, and the answer to late is nothing at all.

idl/lava.npidl:1588

void StartDrag(surfaceId: u32, paths: vector<string>, chip: DragImage) raises(SurfaceNotFound)

Starts a file drag from this window. The pointer must already be down.

A Lava client has no wl_data_device, so it cannot originate a Wayland drag itself. This is the compositor becoming the source: it offers text/uri-list the same way a foreign client does, which is what makes a drop land in another Lava window and in VSCode, Nautilus, and the rest. Incoming drops were already this compositor's job (see TakeDroppedPaths); this is the other direction. Offered as a copy only — see lava::start_file_drag.

chip is what hangs off the cursor: the client's own views, laid out and emitted into a list of their own (see DragImage). The compositor draws it once into a small canvas surface in the scene's drag tree — where a Wayland client's icon surface goes — and after that only moves the surface. One render for the whole drag, however far the pointer travels, and the chip can be anything a view can be. It is not a second window and it is not pixels: a client still has no GPU, and a picture still does not cross NPRPC. No commands is a drag with no chip.

Empty paths is a no-op rather than an error: a client that asked with nothing selected has made a harmless mistake.

Ignored when no button is down, like BeginMove. From the next line the pointer is the compositor's: a synthetic release is delivered so the client does not hold a capture that nothing will ever end.

idl/lava.npidl:1772

bidi_stream<FocusAck, ActiveWindow> SubscribeActiveWindow()

The focused window, now and whenever it changes.

The compositor owns focus, so a panel has no way to know it — the same shape of hole SubscribeInput filled for the pointer. Without it a global menu cannot exist at all: the menu on a panel is the active window's, and nothing else in this system can say which that is.

A stream rather than a call, for what focus is: it changes when the user clicks, not when a panel asks, and a panel polling for it would be a round trip per frame to learn nothing has happened. The first event is the state at subscription, so a panel that starts after the windows do is not blank until the next click.

Bidirectional for the reason SubscribeInput is, and not because the panel has anything to say: the reverse half is what NPRPC's flow control meters and what makes the compositor notice a panel that has gone away, instead of writing focus changes into a socket nobody reads. FocusAck carries the id the panel actually applied, which also makes "the panel is showing the wrong window's menu" a question with an answer.

Nothing here asks for focus. Being told is a panel's business; taking the keyboard from the window the user is typing in is not, and this interface deliberately has no call for it.

idl/lava.npidl:1538

bidi_stream<InputAck, InputEvent> SubscribeInput(surfaceId: u32) raises(SurfaceNotFound)

The reverse channel: the renderer owns the window, so it owns the mouse and the keyboard, and an app that draws into shared memory has no other way to learn a pointer moved.

Bidirectional rather than a plain server stream because the two directions are one conversation. Events go out; the app's InputAck comes back on the same stream, in order, so "how far has the app got" needs no second channel and no correlation. It also gives NPRPC's credit-based flow control something to meter: a client that stops reading stops being sent to, instead of accumulating an unbounded queue of stale mouse positions in the renderer.

The first event a subscriber receives is always a synthetic Resize carrying the window's current size. Without it an app could only learn its size by waiting for the user to drag a border — it would draw at a guessed size until then, which is exactly the black margin this interface exists to remove.

One subscription per surface, rather than one per client that has to be demultiplexed: a client with two windows gets two streams, and each one carries coordinates already in the right space. Multiplexing would mean stamping every event with a surface id and making every client unpick it, to save a stream that costs almost nothing.

The subscription is also the surface's lease. When this stream ends — the client closed it, the client died, the user closed the window — the surface goes with it. Tying the two together is what stops a crashed client from leaving a window on screen that nothing will ever draw into again.

raises here is a stream-initialization failure, and the distinction is the useful part: an unknown surfaceId is a caller mistake, so it fails at the call, with a typed exception, before any stream exists. Aborting the stream instead would report the same mistake one chunk later, as a throw out of the client's for try await — by which point the caller has already built a subscription around something that was never going to work. The line is drawn at first stream access: throw before touching the stream and the client sees SurfaceNotFound; fail after, and it is a stream error like any other.

idl/lava.npidl:1723

bidi_stream<MenuReply, MenuRequest> SubscribeMenu(surfaceId: u32) raises(SurfaceNotFound)

The menu to open, and again every time the user asks for one.

The subscription a context menu client holds: right-clicking the desktop or a window's non-client area produces a MenuRequest here, and the client answers with a MenuReply naming what was chosen. The compositor does the rest — it owns every action on the menu, which is why it can be sure the item that was clicked is still a thing that can be done.

Bidirectional for the reason the other state streams are, and here the reverse half carries real news rather than only flow control: the reply is the result. See SubscribePanelArea for why a server_stream servant is the wrong shape for anything the compositor's loop pushes.

surfaceId is this client's menu surface, from CreateMenuSurface. Passing an ordinary window is refused: the compositor is about to place this surface at the pointer and grab for it, and doing that to somebody's text editor is not a request it should honour.

One subscriber. A second one is refused rather than fanned out to: two processes drawing the desktop's context menu is not a configuration with a right answer, and the failure would show up as two menus and one grab. The session's own menu client is started like the panel and the dock — see [shell] in lava.conf.

A desktop with nobody subscribed simply has no context menu. The right click still clears focus, the way it did before this existed.

idl/lava.npidl:1567

bidi_stream<MenuChoiceAck, MenuChoice> SubscribeMenuChoice(surfaceId: u32) raises(SurfaceNotFound)

Where the answers to this surface's OpenMenu calls arrive.

Per surface, not one-per-desktop like SubscribeMenu: every panel may have menus of its own, and they are told apart by whose surface asked. The stream is the delivery route and the permission at once — see OpenMenu, which refuses to open a menu for a surface with nobody listening.

idl/lava.npidl:2113

bidi_stream<PanelAreaAck, PanelArea> SubscribePanelArea(surfaceId: u32) raises(SurfaceNotFound)

Whether this panel's strip is clear of windows, and again whenever that changes.

For a dock that hides itself: auto-hide is right when a window is in the way and wrong when the desktop behind it is empty, and the difference is something only the compositor can see. A panel knows its own rectangle and nothing about what overlaps it.

Sent on subscription, and afterwards only when the answer changes — not on every window move. Dragging a window across the bottom of the screen is two messages, on the frames it crosses the edge of the strip, however many motion events it took.

bidi_stream like the rest, and the reason is the servant's shape rather than anything the client has to say. A server_stream servant in NPRPC is a generator: it returns a coroutine that the stream manager resumes once per credit and pulls values out of, which is right for a stream that can produce on demand — a file, a video, N rows. This one cannot. Its values are pushed, irregularly, by the compositor's own loop when a window moves, so the coroutine would have to suspend on an awaiter until there is news and whoever resumed it would do the transport write: the Wayland loop, writing into a ring a wedged client has stopped draining, which is the one thing every subscription here is built to avoid. Putting that write back on its own thread means rebuilding StreamPump inside a coroutine.

The bidirectional form hands the servant an ordinary writer instead, and that moves straight into the pump every other subscription already uses. The acks are flow control and liveness, as everywhere else here.

The subscription also ends when the surface does — DestroySurface, or the client exiting — which is the same signal the input stream is torn down by.

idl/lava.npidl:1292

bidi_stream<ThemeAck, SystemTheme> SubscribeSystemTheme()

The system colour theme, and again whenever Settings changes it.

Bidirectional for the same reason the other state streams are, and it is a sharper reason than "the acks are flow control" — see SubscribePanelArea, where it is written out. Briefly: a server_stream servant is a generator the stream manager pulls from, and none of these streams can produce on demand; they are pushed by the compositor when something changes. The bidirectional form is the one that hands the servant a writer it can keep.

The first message is the theme at subscription, so a client that starts after Settings already picked nebula is nebula on its first frame rather than dark-until-someone-clicks.

idl/lava.npidl:1308

bidi_stream<WindowListAck, WindowList> SubscribeWindows()

Every window, and again whenever the set or any of it changes.

The other half of SubscribeActiveWindow, and the one a dock needs: focus says which window is in front, this says which windows exist. Until it, a panel could show a clock and nothing else about the desktop it sat on — and Minimize was a button that hid a window nothing could bring back, because nothing could name it.

Sent on every change that a shell would draw differently: a window opening or closing, a title changing, focus moving, a window being minimized or restored, and a workspace switch. Not on move or resize — this carries no geometry, so nothing a dock draws depends on them.

Bidirectional for the reason the other streams are: the reverse half is what flow control meters and what tells the compositor the shell is still there, not because a shell has anything to say.

idl/lava.npidl:1257

vector<string> TakeDroppedPaths(surfaceId: u32) raises(SurfaceNotFound)

The paths from the oldest drop this surface has not collected yet, and removes it from the queue. Empty if there is none.

Called after a FileDrop event arrives on the input stream: the event carries the position, which is what decides which view was dropped on, and this carries the payload. Two calls rather than one because the payload is a variable-length list and the event stream is a fixed-size struct — and because most events are not drops, and should not pay for the one that is.

A queue, not a slot. GLFW's own drop buffer is valid only until the next drop, so the compositor copies it out the moment it polls the event; keeping a queue is what makes two quick drops two drops rather than the second one overwriting the first before the client has looked. Ordered, so the nth FileDrop event pairs with the nth call.

Take, not get: whoever collects a drop owns it, and a client that asks twice for the same drop is a client that would process the files twice. The queue is per surface, so a client falling behind delays only itself.

idl/lava.npidl:1745

boolean ToggleMaximize(surfaceId: u32) raises(SurfaceNotFound)

Fills the work area, or puts the window back where it was. Returns the state it ended in.

A toggle rather than SetMaximized(bool), because the client does not know which state it is in and has no way to find out: the user can maximize a window from the compositor's own button or a keybinding, and a client tracking its own idea of the answer would drift out of step with the first one of those. The return value is how it paints the button.

The work area, not the output: a maximized window that covered the panel would hide the one thing on screen meant to always be reachable.

idl/lava.npidl:1204