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
ShowMenuand 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