# 06 — Data Device, Selection, DnD (`wl_data_device`) This chapter covers Greenfield's data device / clipboard / drag-and-drop implementation: how `wl_data_device_manager`, `wl_data_source`, `wl_data_offer`, `wl_data_device` are implemented, how clipboard contents are transferred between browser and native apps, and how DnD is coordinated. ## Faithful Wayland model `DataDeviceManager` is a v3 global singleton. `wl_data_device` is per seat-per-client and the implementation lives directly on `Seat` (which implements `WlDataDeviceRequests`). `WaylandDataSource` / `DataOffer` are the standard source/offer pair, ported almost verbatim from weston, including the `chooseAction()` bit-trick for DnD action negotiation. ## The FD-over-WebSocket problem Wayland's `wl_data_offer.receive(mimeType, fd)` / `wl_data_source.send(mimeType, fd)` carry a real UNIX fd. Greenfield solves this with a **`ProxyFD` JSON descriptor**: ``` ProxyFD = { handle: number, type: 'pipe-read' | 'pipe-write' | 'shm' | 'unknown', host: string // proxy's HTTP base URL } ``` Three concrete `InputOutput` / `InputOutputFD` implementations: ### `webInputOutput` — for in-browser web apps Pipes are `MessageChannel` ports, transferred as `ArrayBuffer`s. All "fd" operations are in-browser zero-copy transfers. ### `RemoteInputOutput` — for native apps All fd operations go over a secondary WebSocket to endpoints: `/mkfifo`, `/mkstemp-mmap`, `/read-fd-as-stream`, `/write-fd-as-stream`, `/read-fd`, `/close-fd`. ### `ProxyInputOutput` (proxy-side) — owns the real OS pipes and memfd files `proxyFDtoNativeFD(proxyFD)` returns the raw int fd because every ProxyFD has `host === proxy.baseURL`. The proxy interceptors for `wl_data_source` / `wl_data_offer` / `wl_data_device` / `wl_data_device_manager` are auto-generated no-ops that only handle resource construction and lifetime — all actual semantics live in the browser compositor. ### Crucial consequence For native→native clipboard transfers (e.g. user copies in one Linux app, pastes in another), the browser compositor never sees a single byte of the data — it only relays the fd reference. The actual content stays inside the proxy host's OS kernel pipe. ## MIME-type negotiation At the Wayland level, mime types are just relayed strings. Mapping to the browser's hard-coded allow-list `['text/plain', 'text/html', 'image/png']` happens only at the DOM boundary: ### Native → browser `selection.ts:handleWaylandDataSourceUpdate`: each source mime type is substring-matched against the allow-list (`text/plain;charset=utf-8` contains `text/plain`, so it maps), survivors are pulled as separate blobs via `mkfifo + send + readBlob`, and finally `navigator.clipboard.write([new ClipboardItem(...)])`. ### Browser → native `BrowserDataSource.ts`: `ClipboardItem.types` become the source mime types, plus a synthetic `text/plain;charset=utf-8` alias is appended (Linux apps expect the charset form). On `send`, the alias is normalised back to `text/plain` for the lookup. ### Caveat: substring matching is fragile The substring match `dataSourceMimeType.indexOf(browserMimeType) !== -1` can collide (`image/png` matching `image/png;base64` is fine, but `text/plain;charset=iso-8859-1` silently becomes `text/plain`). Consider an explicit prefix or exact-match policy in your implementation. ## Clipboard mirroring is pull-based The browser has no `clipboardchange` event (Chromium issue 933608 is cited in the code). Greenfield polls `navigator.clipboard.read()`: - on init - on every `window.focus` event De-duplicates offers by mime-type set + total blob size, then calls `seat.setSelectionInternal(createBrowserDataSource(offers), serial)`. The feedback loop is broken because `handleWaylandDataSourceUpdate` returns early when `dataSource instanceof BrowserDataSource`. ### Firefox fallback `text-selection.ts` uses only `readText` / `writeText` and a `BrowserTextDataSource` that serves `text/plain;charset=utf-8` — no images or HTML. ## Drag-and-drop ### Wayland-side (Seat.startDrag) 1. Validates the implicit pointer grab. 2. Attaches `DragIconRole` to the optional icon surface. 3. Installs a `PointerDrag` grab on both pointer and keyboard. `PointerDrag` tracks the focused data device, sends `wl_data_device.enter` / `leave` / `motion` / `drop`, creates fresh `wl_data_offer`s via `sendOffer`, and runs the v3 action negotiation. ### Browser-side (`BrowserDnD` class) Creates a hidden full-canvas `dragHelper` div with `draggable=true` when the Wayland drag-icon surface commits, so the browser shows the Wayland icon via `event.dataTransfer.setDragImage(cursorCanvas, ...)`. While the user drags, `dragover` events on the dragHelper are converted back to `ButtonEvent`s and pushed through `session.inputQueue.queueMotion`, keeping `PointerDrag` fed. ### Not implemented: dragging data FROM a Wayland app INTO a real DOM target The FIXME in `dnd.ts` admits: dragging data FROM a Wayland app INTO a real DOM drop target (file drag-out, etc.) is **not implemented**. ## XWayland bridge `XWindowManager` listens on the X11 `CLIPBOARD` atom via `XFixes` and builds `XDataSource` (clipboard) or `XDnDDataSource` (XDnD). When the receiver pulls data: 1. `XDataSource.send` calls `XConvertSelection` to have the X server push the bytes into a property on `selectionWindow`. 2. `writeProperty` copies them into the receiver's `InputOutputFD`. 3. INCR transfers are handled (`getIncrChunk`, `sendIncrChunk`). ## Primary selection is NOT implemented A grep for `primary_selection`, `gtk_primary`, `zwp_primary_selection` across the entire `packages/` tree returns zero hits. XWayland only watches the `CLIPBOARD` atom, not `PRIMARY`. **Middle-click paste does not work in Greenfield today.** **For the user**: if your users expect Linux-style middle-click paste, you'll need to implement `zwp_primary_selection_device_manager_v1` (or `gtk_primary_selection`) on the compositor side and forward `PRIMARY` notifications in `XWindowManager`. ## ASCII diagram — clipboard flow in both directions ``` CLIPBOARD / SELECTION FLOW ================================================================= A) NATIVE APP COPIES ──► BROWSER DOM CLIPBOARD (Ctrl+C in Linux app) ================================================================= ┌──────────────┐ wl_data_device.set_selection(src,serial) ┌────────────────────────┐ │ Native app │ ─────────────────────────────────────────────► │ compositor │ │ (Wayland or │ │ (browser, in DOM) │ │ XWayland) │ ◄────── wl_data_offer.offer("text/plain") ─── │ │ └──────────────┘ (only if this app has keyboard focus)└───────────┬────────────┘ │ selectionListeners fire ▼ ┌──────────────────────────┐ │ handleWaylandDataSource │ │ Update() (selection.ts)│ └──────────┬──────────────┘ mkfifo() ──────────────────────────► │ proxy /mkfifo ProxyFD read+write (real OS pipe) ◄───── returns 2 ProxyFDs ▼ dataSource.send("text/plain", writeFD) │ ▼ wl_data_source.send("text/plain", writeFDProxyFD) ─────────────────────────────────────────► │ Native source │ receives real write fd │ (proxyFDtoNativeFD │ is a passthrough) ▼ Native app writes bytes ──────────────► OS pipe ───┐ │ readFD.readBlob() ◄──────────── read from /read-fd-as-stream ◄─┘ ▼ (proxy streams over WS) Blob {type:"text/plain"} ▼ navigator.clipboard.write([ new ClipboardItem({"text/plain": blob}) ]) ▼ ┌────────────────────────────┐ │ DOM clipboard populated │ │ (Ctrl+V anywhere now works) │ └────────────────────────────┘ ================================================================= B) USER COPIES IN BROWSER ──► NATIVE APP GETS SELECTION (Ctrl+C in DOM) ================================================================= ┌─────────────────────────────┐ │ User copies in DOM │ │ (browser tab, textarea...) │ └──────────────┬──────────────┘ │ on window 'focus' event (or initial load) ▼ navigator.clipboard.read() ──► ClipboardItems[] ▼ handleBrowserDataSourceUpdate() — de-dup vs previous offers ▼ createBrowserDataSource(offers) .mimeTypes = offer.types + ['text/plain;charset=utf-8'] alias ▼ seat.setSelectionInternal(browserDataSource, serial) │ │ 1) selectionListeners fire — handleWaylandDataSourceUpdate │ returns early (instanceof BrowserDataSource), breaking loop │ │ 2) sendSelection(focusedClient) ▼ ┌────────────────────────┐ wl_data_offer.offer("text/plain"), ┌──────────────┐ │ compositor │ ─ wl_data_device.selection(offer) ──► │ Native app │ │ (browser, in DOM) │ │ (focused) │ └───────────┬────────────┘ └──────┬───────┘ │ │ │ When the native app calls wl_data_offer.receive("text/plain", fd): │ │ │ ◄──────── wl_data_offer.receive(mimeType, writeFDProxyFD) ◄── native pipe │ (fd was created by the native app's libwayland; proxy │ wrapped it as ProxyFD{host:proxy.baseURL, type:'unknown'} │ then compositor's DataOffer.receive calls wrapFD → RemoteInputOutputFD) ▼ BrowserDataSource.send(mimeType, ioFD) │ ▼ matchingOffer.getType(mimeType) ─► Promise (from ClipboardItem) │ ▼ ioFD.write(blob) ─► POST /write-fd-as-stream?fd= ─► proxy │ ▼ createWriteStream({fd, autoClose:true}) ws stream ──► native write fd │ ▼ Bytes flow through OS pipe into native app │ ▼ ioFD.close() ─► /close-fd ─► EOF ─► native app's read returns 0 ================================================================= PROXY FD TRANSLATION (works the same way in B and A) ================================================================= Native fd (in proxy process) │ │ wrap into ProxyFD JSON: {handle, type, host: proxy.baseURL} │ JSON-encode, length-prefix, send over WS protocol channel ▼ ┌─────────────────────────────────────────────────┐ │ Browser compositor │ │ client.userData.inputOutput.wrapFD(fd, type) │ │ → RemoteInputOutputFD (RemoteInputOutput) │ │ → WebPipeInputOutputFD (webInputOutput, │ │ for in-browser apps)│ └─────────────────────────────────────────────────┘ ``` ## File reference | File | Role | |---|---| | `packages/compositor/src/DataDeviceManager.ts` | `wl_data_device_manager` global | | `packages/compositor/src/DataSource.ts` | `wl_data_source` impl, the provider side | | `packages/compositor/src/DataOffer.ts` | `wl_data_offer` impl, the receiver side | | `packages/compositor/src/WaylandDataSource.ts` | Wrapper around native `wl_data_source` events | | `packages/compositor/src/Seat.ts` | `wl_data_device` impl (Seat implements `WlDataDeviceRequests`) | | `packages/compositor/src/browser/selection.ts` | Browser clipboard bridge (native → DOM) | | `packages/compositor/src/browser/text-selection.ts` | Firefox fallback (text-only) | | `packages/compositor/src/browser/dnd.ts` | Browser DnD bridge (Wayland → DOM) | | `packages/compositor/src/InputOutput.ts` | FD abstraction interface | | `packages/compositor/src/remote/RemoteInputOutput.ts` | FD-over-WebSocket for native apps | | `packages/compositor/src/remote/XWindowManager.ts` | X11 CLIPBOARD atom watcher | | `packages/compositor/src/remote/XDataSource.ts` | X11 clipboard source (XWayland apps) | | `packages/compositor/src/remote/XDnDDataSource.ts` | X11 DnD source | ## Next actions / suggested improvements 1. **Primary selection** — implement `zwp_primary_selection_device_manager` v1 (or `gtk_primary_selection`) on the compositor side and forward `PRIMARY` notifications in `XWindowManager`. This unlocks middle-click paste from native apps. 2. **`clipboardchange` event** — when browsers ship it, replace the `window.focus`-based polling in `selection.ts` with a proper event-driven subscription. 3. **Native→DOM DnD** — implement the dataTransfer side of `BrowserDnD` so a Wayland drag can deliver bytes to a real DOM drop target (the `dnd.ts` FIXME). 4. **MIME allow-list discovery** — replace the hard-coded `['text/plain', 'text/html', 'image/png']` with a runtime check once browsers expose the supported types. 5. **MIME-type strict matching** — the substring match `dataSourceMimeType.indexOf(browserMimeType) !== -1` can collide (`image/png` matching `image/png;base64` is fine, but `text/plain;charset=iso-8859-1` silently becomes `text/plain`). Consider an explicit prefix or exact-match policy.