// F16 clipboard + DnD demo — wl_data_device_manager on top of the xdg-shell
// scaffold (../scaffold/app.d; findings: ../../f16-clipboard-dnd.md).
//
// ONE client, TWO toplevels (A = wsi-f16-a, the source; B = wsi-f16-b, the
// drop target) — clipboard and drag-and-drop are the SAME machinery
// (wl_data_source / wl_data_offer over pipe fds), exercised end to end:
//
// stale probe at startup, before ANY input event exists, a wl_data_source
// is offered and wl_data_device.set_selection(serial=0) is
// attempted — the Wayland divergence under test: the clipboard
// cannot be claimed without a recent input serial. ./run.sh
// then proves with `wl-paste` (a real second client) that no
// selection was installed.
// copy an injected right-click on A yields a REAL serial; a new
// source offering text/plain;charset=utf-8 + text/plain
// (payload é漢🎈) is installed; `wl-paste -l` / `wl-paste`
// read it back (the demo serves wl_data_source.send by
// writing the fd) — then `wl-copy` takes the selection over
// and the demo logs wl_data_source.cancelled (ownership loss).
// paste when a wtype-plugged keyboard gives the client keyboard
// focus, wl_data_device.data_offer + selection deliver
// wl-copy's offer; the demo logs the offered MIME types and
// receives the bytes through its own pipe.
// dnd an injected left-press on A starts
// wl_data_device.start_drag (actions copy|move) WHILE the
// button is held; the injector sweeps the pointer into B and
// releases. Both sides of the negotiation are one process,
// so the full sequence is in one log: enter(A)/motion/leave,
// enter(B), offer accept + set_actions(preferred=move),
// source_actions/action, drop, receive-pipe transfer,
// finish, dnd_drop_performed, dnd_finished.
//
// Headless-safe: no compositor → SKIP, exit 0; a seat-less compositor
// (headless weston) logs the registry probe and exits 0.
module (module) appapp;
import c; // ImportC: wayland-client + xdg-shell glue + pipe/poll
import instrument;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdioD header file for C99 <stdio.h>
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdio.h.html, stdio.h
Source
core/stdc/stdio.d
stdio : (alias) app.printf = int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stdlibD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/stdlib.h.html, stdlib.h
Source
core/stdc/stdlib.d
stdlib : (alias) app.atoi = int core.stdc.stdlib.atoi(scope const(char*) nptr) nothrow @nogcatoi, (alias) app.getenv = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv;
import (package) corecore.(package) core.stdcstdc.(module) core.stdc.stringD header file for C99.
pubs.opengroup.org/onlinepubs/009695399/basedefs/string.h.html, string.h
Source
core/stdc/string.d
string : (alias) app.strcmp = int core.stdc.string.strcmp(scope const(char*) s1, scope const(char*) s2) pure nothrow @nogcstrcmp, (alias) app.strlen = ulong core.stdc.string.strlen(scope const(char*) s) pure nothrow @nogcstrlen, (alias) app.strncpy = char* core.stdc.string.strncpy(return scope char* s1, scope const(char*) s2, ulong n) pure nothrow @nogcstrncpy;
enum int (constant) int app.winWidth = 640winWidth = 640;
enum int (constant) int app.winHeight = 480winHeight = 480;
enum uint (constant) uint app.BTN_LEFT = 272uBTN_LEFT = 0x110, (constant) uint app.BTN_RIGHT = 273uBTN_RIGHT = 0x111;
__gshared immutable char* (immutable global) immutable(char*) app.mimeUtf8mimeUtf8 = "text/plain;charset=utf-8";
__gshared immutable char* (immutable global) immutable(char*) app.mimePlainmimePlain = "text/plain";
__gshared immutable char* (immutable global) immutable(char*) app.clipPayloadclipPayload = "é漢🎈"; // 9 UTF-8 bytes
__gshared immutable char* (immutable global) immutable(char*) app.dndPayloaddndPayload = "dnd-payload-é漢🎈";
// -------------------------------------------------------------------- state
struct (struct) app.BufferBuffer
{
wl_buffer* (field) _error_ app.Buffer.handlehandle;
uint* (field) uint* app.Buffer.pixelspixels;
(alias) object.size_t = ulongsize_t (field) ulong app.Buffer.byteSizebyteSize;
int (field) int app.Buffer.widthwidth, (field) int app.Buffer.heightheight;
bool (field) bool app.Buffer.busybusy;
}
struct (struct) app.SurfSurf
{
wl_surface* (field) _error_ app.Surf.surfacesurface;
xdg_surface* (field) _error_ app.Surf.xdgSurfacexdgSurface;
xdg_toplevel* (field) _error_ app.Surf.topleveltoplevel;
(struct) app.BufferBuffer[2] (field) _error_ app.Surf.buffersbuffers;
int (field) int app.Surf.widthwidth = (constant) int app.winWidth = 640winWidth, (field) int app.Surf.heightheight = (constant) int app.winHeight = 480winHeight;
int (field) int app.Surf.pendingWpendingW, (field) int app.Surf.pendingHpendingH;
bool (field) bool app.Surf.configuredconfigured;
}
/// One in-flight wl_data_offer.receive transfer (read end of the pipe).
struct (struct) app.PipeOne in-flight wl_data_offer.receive transfer (read end of the pipe).
Pipe
{
int (field) int app.Pipe.fdfd = -1;
wl_data_offer* (field) _error_ app.Pipe.offeroffer;
char[4096] (field) char[4096] app.Pipe.bufbuf;
(alias) object.size_t = ulongsize_t (field) ulong app.Pipe.lenlen;
}
/// MIME types announced by a wl_data_offer before its enter/selection event.
struct (struct) app.OfferInfoMIME types announced by a wl_data_offer before its enter/selection event.
OfferInfo
{
wl_data_offer* (field) _error_ app.OfferInfo.offeroffer;
char[64][8] (field) char[64][8] app.OfferInfo.mimesmimes; // up to 8 MIME strings of 63 chars
int (field) int app.OfferInfo.mimeCountmimeCount;
uint (field) uint app.OfferInfo.sourceActionssourceActions;
}
enum (alias) object.size_t = ulongsize_t (constant) ulong app.IDX_A = 0LUIDX_A = 0, (constant) ulong app.IDX_B = 1LUIDX_B = 1;
__gshared immutable(char*)[2] (immutable global) immutable(char*[2]) app.g_surfNamesg_surfNames = ["A", "B"];
__gshared
{
wl_display* _error_ app.g_displayg_display;
wl_registry* _error_ app.g_registryg_registry;
wl_compositor* _error_ app.g_compositorg_compositor;
wl_shm* _error_ app.g_shmg_shm;
xdg_wm_base* _error_ app.g_wmBaseg_wmBase;
wl_seat* _error_ app.g_seatg_seat;
wl_pointer* _error_ app.g_pointerg_pointer;
wl_keyboard* _error_ app.g_keyboardg_keyboard;
wl_data_device_manager* _error_ app.g_ddmg_ddm;
uint (__gshared global) uint app.g_ddmVersiong_ddmVersion;
wl_data_device* _error_ app.g_dataDeviceg_dataDevice;
wl_data_source* _error_ app.g_selSourceg_selSource; // currently installed selection source
wl_data_source* _error_ app.g_staleSourceg_staleSource; // the serial=0 probe source
wl_data_source* _error_ app.g_dragSourceg_dragSource;
wl_data_offer* _error_ app.g_dndOfferg_dndOffer; // current drag offer (target side)
(struct) app.OfferInfoMIME types announced by a wl_data_offer before its enter/selection event.
OfferInfo[4] _error_ app.g_offersg_offers;
(struct) app.SurfSurf[2] _error_ app.g_surfsg_surfs;
int (__gshared global) int app.g_focusIdxg_focusIdx = -1;
double (__gshared global) double app.g_ptrXg_ptrX, (__gshared global) double app.g_ptrYg_ptrY;
uint (__gshared global) uint app.g_dndEnterSerialg_dndEnterSerial;
int (__gshared global) int app.g_dndMotionsg_dndMotions;
(struct) app.PipeOne in-flight wl_data_offer.receive transfer (read end of the pipe).
Pipe[2] _error_ app.g_pipesg_pipes; // [0] selection paste, [1] dnd drop — independent
bool (__gshared global) bool app.g_runningg_running = true, (__gshared global) bool app.g_autoExitg_autoExit, (__gshared global) bool app.g_staleProbeSentg_staleProbeSent;
bool (__gshared global) bool app.g_dndFinishedg_dndFinished;
long (__gshared global) long app.g_runUsCapg_runUsCap = 2_500_000;
long (__gshared global) long app.g_firstPaintUsg_firstPaintUs = -1, (__gshared global) long app.g_exitAtUsg_exitAtUs = -1;
int (__gshared global) int app.g_framesg_frames;
}
// ---------------------------------------------------------- offer registry
(struct) app.OfferInfoMIME types announced by a wl_data_offer before its enter/selection event.
OfferInfo* app.offerInfoofferInfo(wl_data_offer* o) nothrow @nogc
{
foreach (ref (parameter) oioi; g_offers)
if (oi.oi.offeroffer is o)
return &oi;
return null;
}
(struct) app.OfferInfoMIME types announced by a wl_data_offer before its enter/selection event.
OfferInfo* app.offerInfoNewofferInfoNew(wl_data_offer* o) nothrow @nogc
{
foreach (ref (parameter) oioi; g_offers)
if (oi.oi.offeroffer is null)
{
oi.oi.offeroffer = o;
oi.oi.mimeCountmimeCount = 0;
oi.oi.sourceActionssourceActions = 0;
return &oi;
}
g_offers[0].g_offers[0].offeroffer = o; // recycle the oldest slot
g_offers[0].g_offers[0].mimeCountmimeCount = 0;
g_offers[0].g_offers[0].sourceActionssourceActions = 0;
return &g_offers[0];
}
void app.offerInfoDropofferInfoDrop(wl_data_offer* o) nothrow @nogc
{
foreach (ref (parameter) oioi; g_offers)
if (oi.oi.offeroffer is o)
oi.oi.offeroffer = null;
}
void app.logOfferMimeslogOfferMimes(const(char)* kind, (struct) app.OfferInfoMIME types announced by a wl_data_offer before its enter/selection event.
OfferInfo* (parameter) OfferInfo* oioi) nothrow @nogc
{
instrEvent(kind, "mime_count=%d", oi !is null ? oi.oi.mimeCountmimeCount : -1);
if (oi !is null)
foreach ((parameter) ii; 0 .. oi.oi.mimeCountmimeCount)
instrEvent("offer_mime", "i=%d mime=%s", i, oi.oi.mimesmimes[i].ptr);
}
bool app.offerHasofferHas((struct) app.OfferInfoMIME types announced by a wl_data_offer before its enter/selection event.
OfferInfo* (parameter) OfferInfo* oioi, const(char)* mime) nothrow @nogc
{
if (oi is null)
return false;
foreach ((parameter) ii; 0 .. oi.oi.mimeCountmimeCount)
if (strcmp(oi.oi.mimesmimes[i].ptr, mime) == 0)
return true;
return false;
}
// ------------------------------------------------------------ data source
extern (C) void app.onSourceTargetonSourceTarget(void* data, wl_data_source* (parameter) wl_data_source* ss, const(char)* mime) nothrow @nogc
{
// DnD only: the target's current accept state (null = would reject).
instrEvent("source_target", "mime=%s", mime !is null ? mime : "(null)");
}
extern (C) void app.onSourceSendonSourceSend(void* data, wl_data_source* (parameter) wl_data_source* ss, const(char)* mime, int (parameter) int fdfd) nothrow @nogc
{
const(char)* _error_ payloadpayload = s is g_dragSource ? dndPayload : clipPayload;
immutable _error_ lenlen = strlen(payload);
immutable _error_ writtenwritten = write(fd, payload, len);
close(fd);
instrEvent("clip_send", "source=%s fmt=%s bytes=%lld",
s is g_dragSource ? "dnd".ptr : "selection".ptr, mime, cast(long) written);
}
extern (C) void app.onSourceCancelledonSourceCancelled(void* data, wl_data_source* (parameter) wl_data_source* ss) nothrow @nogc
{
// Selection: ownership loss (another client set the selection).
// DnD: the drag was rejected/ended without a successful drop.
instrEvent("ownership_lost", "source=%s",
s is g_dragSource ? "dnd".ptr : (s is g_staleSource ? "stale".ptr : "selection".ptr));
if (s is g_selSource)
g_selSource = null;
else if (s is g_staleSource)
g_staleSource = null;
else if (s is g_dragSource)
g_dragSource = null;
wsi_data_source_destroy(s);
}
extern (C) void app.onSourceDndDropPerformedonSourceDndDropPerformed(void* data, wl_data_source* (parameter) wl_data_source* ss) nothrow @nogc
{
instrEvent("source_dnd_drop_performed");
}
extern (C) void app.onSourceDndFinishedonSourceDndFinished(void* data, wl_data_source* (parameter) wl_data_source* ss) nothrow @nogc
{
instrEvent("source_dnd_finished", "note=target-done-source-may-delete-on-move");
if (s is g_dragSource)
g_dragSource = null;
wsi_data_source_destroy(s);
g_dndFinished = true;
g_exitAtUs = instrNowUs() + 1_000_000; // wind down shortly after
}
extern (C) void app.onSourceActiononSourceAction(void* data, wl_data_source* (parameter) wl_data_source* ss, uint action) nothrow @nogc
{
instrEvent("source_action", "dnd_action=%u", action);
}
__gshared wl_data_source_listener _error_ app.g_sourceListenerg_sourceListener = {
&onSourceTarget, &onSourceSend, &onSourceCancelled,
&onSourceDndDropPerformed, &onSourceDndFinished, &onSourceAction
};
wl_data_source* app.makeTextSourcemakeTextSource(bool forDnd) nothrow @nogc
{
wl_data_source* _error_ ss = wsi_ddm_create_data_source(g_ddm);
wsi_data_source_add_listener(s, &g_sourceListener, null);
wsi_data_source_offer(s, mimeUtf8);
wsi_data_source_offer(s, mimePlain);
if (forDnd && g_ddmVersion >= 3)
wsi_data_source_set_actions(s,
WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE);
instrEvent("clip_offer", "kind=%s formats=[%s,%s]%s",
forDnd ? "dnd".ptr : "selection".ptr, mimeUtf8, mimePlain,
forDnd ? " actions=copy|move".ptr : "".ptr);
return s;
}
// ------------------------------------------------------------- data offer
extern (C) void app.onOfferOfferonOfferOffer(void* data, wl_data_offer* o, const(char)* mime) nothrow @nogc
{
(unresolved type) OfferInfoOfferInfo* _error_ oioi = offerInfo(o);
if (oi !is null && oi.oi.mimeCountmimeCount < oi.oi.mimesmimes.oi.mimes.lengthlength)
{
strncpy(oi.oi.mimesmimes[oi.oi.mimeCountmimeCount].ptr, mime, 63);
oi.oi.mimesmimes[oi.oi.mimeCountmimeCount][63] = '\0';
oi.oi.mimeCountmimeCount++;
}
}
extern (C) void app.onOfferSourceActionsonOfferSourceActions(void* data, wl_data_offer* o, uint actions) nothrow @nogc
{
(unresolved type) OfferInfoOfferInfo* _error_ oioi = offerInfo(o);
if (oi !is null)
oi.oi.sourceActionssourceActions = actions;
instrEvent("offer_source_actions", "actions=%u", actions);
}
extern (C) void app.onOfferActiononOfferAction(void* data, wl_data_offer* o, uint action) nothrow @nogc
{
// The compositor's pick from source-actions ∩ target-actions (and the
// target's preference) — who chose copy vs move, answered here.
instrEvent("offer_action", "dnd_action=%u note=compositor-resolved", action);
}
__gshared wl_data_offer_listener _error_ app.g_offerListenerg_offerListener = {
&onOfferOffer, &onOfferSourceActions, &onOfferAction
};
/// Begin an fd transfer from `o` (slot 0 = selection paste, slot 1 = dnd
/// post-drop read); the pipe's read end joins the poll loop and is drained
/// to EOF there. The two slots are independent — a selection offer
/// re-delivered right after a drop must not clobber the dnd transfer.
void app.receiveFromBegin an fd transfer from o (slot 0 = selection paste, slot 1 = dnd
post-drop read); the pipe's read end joins the poll loop and is drained
to EOF there. The two slots are independent — a selection offer
re-delivered right after a drop must not clobber the dnd transfer.
receiveFrom(wl_data_offer* o, bool (parameter) bool isDndisDnd) nothrow @nogc
{
(unresolved type) PipePipe* _error_ pp = &g_pipes[isDnd ? 1 : 0];
if (p.p.fdfd >= 0)
{
close(p.p.fdfd);
p.p.fdfd = -1;
}
int[2] _error_ fdsfds;
if (wsi_pipe2_cloexec(fds.ptr) != 0)
return;
wsi_data_offer_receive(o, mimeUtf8, fds[1]);
close(fds[1]);
wl_display_flush(g_display);
p.p.fdfd = fds[0];
p.p.offeroffer = o;
p.p.lenlen = 0;
instrEvent("clip_request", "kind=%s fmt=%s", isDnd ? "dnd".ptr : "selection".ptr, mimeUtf8);
}
void void app.drainPipe(bool isDnd) nothrow @nogcdrainPipe(bool (parameter) bool isDndisDnd) nothrow @nogc
{
(struct) app.PipeOne in-flight wl_data_offer.receive transfer (read end of the pipe).
Pipe* (local variable) _error_ pp = &g_pipes[isDnd ? 1 : 0];
immutable (local variable) immutable(_error_) nn = read(p.p.fdfd, p.p.bufbuf.ptr + p.p.lenlen, p.p.bufbuf.p.buf.lengthlength - 1 - p.p.lenlen);
if (n > 0)
{
p.(field) _error_ p.lenlen += n;
return;
}
close(p.p.fdfd);
p.p.fdfd = -1;
p.(field) _error_ p.bufbuf[p.p.lenlen] = '\0';
instrEvent(isDnd ? "dnd_drop_data" : "paste_data",
"bytes=%lld text=%s", cast(long) p.p.lenlen, p.p.bufbuf.ptr);
if ((parameter) bool isDndisDnd && p.(field) _error_ p.offeroffer !is null)
{
if ((__gshared global) uint app.g_ddmVersiong_ddmVersion >= 3)
wsi_data_offer_finish(p.p.offeroffer); // "the drag-and-drop operation ended successfully"
instrEvent("dnd_finish_sent");
wsi_data_offer_destroy(p.p.offeroffer);
offerInfoDrop(p.p.offeroffer);
if (g_dndOffer is p.(field) _error_ p.offeroffer)
g_dndOffer = null;
}
p.p.offeroffer = null;
}
// ------------------------------------------------------------- data device
extern (C) void app.onDataOfferonDataOffer(void* data, wl_data_device* d, wl_data_offer* o) nothrow @nogc
{
// A new offer object: its MIME list arrives as offer events BEFORE the
// enter/selection event that tells us what it is for.
offerInfoNew(o);
wsi_data_offer_add_listener(o, &g_offerListener, null);
instrEvent("data_offer", "offer=%p", o);
}
extern (C) void app.onDndEnteronDndEnter(void* data, wl_data_device* d, uint serial,
wl_surface* (parameter) wl_surface* surfacesurface, int (parameter) int xx, int (parameter) int yy, wl_data_offer* o) nothrow @nogc
{
immutable _error_ idxidx = surfIndexOf(surface);
g_dndEnterSerial = serial;
g_dndOffer = o;
(unresolved type) OfferInfoOfferInfo* _error_ oioi = offerInfo(o);
instrEvent("dnd_enter", "surface=%s serial=%u pos=%.1f,%.1f",
idx >= 0 ? g_surfNames[idx] : "other", serial, x / 256.0, y / 256.0);
logOfferMimes("dnd_enter_formats", oi);
if (o !is null && offerHas(oi, mimeUtf8))
{
// Target-side negotiation: accept the mime, then declare our action
// set + preference (the compositor resolves the final dnd_action).
wsi_data_offer_accept(o, serial, mimeUtf8);
if (g_ddmVersion >= 3)
wsi_data_offer_set_actions(o,
WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY | WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE,
WL_DATA_DEVICE_MANAGER_DND_ACTION_MOVE);
instrEvent("dnd_accept", "serial=%u fmt=%s actions=copy|move preferred=move",
serial, mimeUtf8);
}
}
extern (C) void app.onDndLeaveonDndLeave(void* data, wl_data_device* d) nothrow @nogc
{
instrEvent("dnd_leave", "motions_seen=%d", g_dndMotions);
g_dndOffer = null;
}
extern (C) void app.onDndMotiononDndMotion(void* data, wl_data_device* d, uint time, int (parameter) int xx, int (parameter) int yy) nothrow @nogc
{
g_dndMotions++;
if (g_dndMotions <= 2 || g_dndMotions % 4 == 0)
instrEvent("dnd_motion", "n=%d pos=%.1f,%.1f", g_dndMotions, x / 256.0, y / 256.0);
}
extern (C) void app.onDndDroponDndDrop(void* data, wl_data_device* d) nothrow @nogc
{
instrEvent("dnd_drop", "have_offer=%d", g_dndOffer !is null ? 1 : 0);
if (g_dndOffer !is null)
receiveFrom(g_dndOffer, true);
}
extern (C) void app.onSelectiononSelection(void* data, wl_data_device* d, wl_data_offer* o) nothrow @nogc
{
if (o is null)
{
instrEvent("selection", "offer=null note=selection-cleared");
return;
}
(unresolved type) OfferInfoOfferInfo* _error_ oioi = offerInfo(o);
instrEvent("selection", "offer=%p own_source=%d", o, g_selSource !is null ? 1 : 0);
logOfferMimes("selection_formats", oi);
if (g_selSource is null && offerHas(oi, mimeUtf8))
receiveFrom(o, false); // paste: another client's clipboard
else
{
wsi_data_offer_destroy(o);
offerInfoDrop(o);
}
}
__gshared wl_data_device_listener _error_ app.g_dataDeviceListenerg_dataDeviceListener = {
&onDataOffer, &onDndEnter, &onDndLeave, &onDndMotion, &onDndDrop, &onSelection
};
// ----------------------------------------------------------------- pointer
int app.surfIndexOfsurfIndexOf(wl_surface* (parameter) wl_surface* ss) nothrow @nogc
{
foreach ((parameter) ii, ref (parameter) sfsf; g_surfs)
if (sf.sf.surfacesurface is s)
return cast(int) i;
return -1;
}
extern (C) void app.onPointerEnteronPointerEnter(void* data, wl_pointer* (parameter) wl_pointer* pp, uint serial,
wl_surface* surf, int sx, int sy) nothrow @nogc
{
g_focusIdx = surfIndexOf(surf);
g_ptrX = sx / 256.0;
g_ptrY = sy / 256.0;
instrEvent("pointer_enter", "surface=%s serial=%u pos=%.1f,%.1f",
g_focusIdx >= 0 ? g_surfNames[g_focusIdx] : "other", serial, g_ptrX, g_ptrY);
}
extern (C) void app.onPointerLeaveonPointerLeave(void* data, wl_pointer* (parameter) wl_pointer* pp, uint serial, wl_surface* surf) nothrow @nogc
{
g_focusIdx = -1;
}
extern (C) void app.onPointerMotiononPointerMotion(void* data, wl_pointer* (parameter) wl_pointer* pp, uint time, int sx, int sy) nothrow @nogc
{
g_ptrX = sx / 256.0;
g_ptrY = sy / 256.0;
}
extern (C) void app.onPointerButtononPointerButton(void* data, wl_pointer* (parameter) wl_pointer* pp, uint serial, uint time,
uint button, uint state) nothrow @nogc
{
instrEvent("button", "surface=%s serial=%u button=0x%x state=%u",
g_focusIdx >= 0 ? g_surfNames[g_focusIdx] : "none", serial, button, state);
if (state != WL_POINTER_BUTTON_STATE_PRESSED || g_focusIdx != IDX_A)
return;
if (button == BTN_RIGHT)
{
// COPY with a REAL input serial — the valid counterpart of the
// startup stale-serial probe.
if (g_selSource !is null)
{
wsi_data_source_destroy(g_selSource);
g_selSource = null;
}
g_selSource = makeTextSource(false);
wsi_data_device_set_selection(g_dataDevice, g_selSource, serial);
instrEvent("set_selection", "serial=%u note=valid-input-serial", serial);
}
else if (button == BTN_LEFT && g_dragSource is null)
{
// DnD: start_drag must carry the serial of an implicit grab — the
// injector holds BTN_LEFT down through the whole sweep.
g_dragSource = makeTextSource(true);
wsi_data_device_start_drag(g_dataDevice, g_dragSource,
g_surfs[IDX_A].g_surfs[IDX_A].surfacesurface, null, serial);
instrEvent("dnd_start", "origin=A serial=%u icon=none", serial);
}
}
extern (C) void app.onPointerAxisonPointerAxis(void* data, wl_pointer* (parameter) wl_pointer* pp, uint time, uint axis, int value) nothrow @nogc {}
extern (C) void app.onPointerFrameonPointerFrame(void* data, wl_pointer* (parameter) wl_pointer* pp) nothrow @nogc {}
extern (C) void app.onPointerAxisSourceonPointerAxisSource(void* data, wl_pointer* (parameter) wl_pointer* pp, uint src) nothrow @nogc {}
extern (C) void app.onPointerAxisStoponPointerAxisStop(void* data, wl_pointer* (parameter) wl_pointer* pp, uint time, uint axis) nothrow @nogc {}
extern (C) void app.onPointerAxisDiscreteonPointerAxisDiscrete(void* data, wl_pointer* (parameter) wl_pointer* pp, uint axis, int discrete) nothrow @nogc {}
extern (C) void app.onPointerAxisValue120onPointerAxisValue120(void* data, wl_pointer* (parameter) wl_pointer* pp, uint axis, int v120) nothrow @nogc {}
extern (C) void app.onPointerAxisRelDironPointerAxisRelDir(void* data, wl_pointer* (parameter) wl_pointer* pp, uint axis, uint dir) nothrow @nogc {}
__gshared wl_pointer_listener _error_ app.g_pointerListenerg_pointerListener = {
&onPointerEnter, &onPointerLeave, &onPointerMotion, &onPointerButton,
&onPointerAxis, &onPointerFrame, &onPointerAxisSource, &onPointerAxisStop,
&onPointerAxisDiscrete, &onPointerAxisValue120, &onPointerAxisRelDir
};
// ---------------------------------------------------------------- keyboard
extern (C) void app.onKeymaponKeymap(void* data, wl_keyboard* kb, uint format, int (parameter) int fdfd, uint (parameter) uint sizesize) nothrow @nogc
{
close(fd);
}
extern (C) void app.onKeyboardEnteronKeyboardEnter(void* data, wl_keyboard* kb, uint serial,
wl_surface* surf, wl_array* keys) nothrow @nogc
{
// The selection offer is delivered around THIS moment — "when a client
// gains keyboard focus" is the delivery contract for selection events.
immutable _error_ idxidx = surfIndexOf(surf);
instrEvent("keyboard_enter", "surface=%s serial=%u",
idx >= 0 ? g_surfNames[idx] : "other", serial);
}
extern (C) void app.onKeyboardLeaveonKeyboardLeave(void* data, wl_keyboard* kb, uint serial, wl_surface* surf) nothrow @nogc {}
extern (C) void app.onKeyonKey(void* data, wl_keyboard* kb, uint serial, uint time, uint key, uint state) nothrow @nogc
{
instrEvent("key", "key=%u state=%u serial=%u", key, state, serial);
}
extern (C) void app.onModifiersonModifiers(void* data, wl_keyboard* kb, uint serial,
uint depressed, uint latched, uint locked, uint group) nothrow @nogc {}
extern (C) void app.onRepeatInfoonRepeatInfo(void* data, wl_keyboard* kb, int rate, int delay) nothrow @nogc {}
__gshared wl_keyboard_listener _error_ app.g_keyboardListenerg_keyboardListener = {
&onKeymap, &onKeyboardEnter, &onKeyboardLeave, &onKey, &onModifiers, &onRepeatInfo
};
// ------------------------------------------------------------------- seat
extern (C) void app.onSeatCapabilitiesonSeatCapabilities(void* data, wl_seat* (parameter) wl_seat* ss, uint caps) nothrow @nogc
{
instrEvent("seat_capabilities", "caps=%u", caps);
immutable _error_ hasPointerhasPointer = (caps & WL_SEAT_CAPABILITY_POINTER) != 0;
immutable _error_ hasKeyboardhasKeyboard = (caps & WL_SEAT_CAPABILITY_KEYBOARD) != 0;
if (hasPointer && g_pointer is null)
{
g_pointer = wsi_seat_get_pointer(g_seat);
wsi_pointer_add_listener(g_pointer, &g_pointerListener, null);
}
else if (!hasPointer && g_pointer !is null)
{
wsi_pointer_destroy(g_pointer);
g_pointer = null;
g_focusIdx = -1;
}
if (hasKeyboard && g_keyboard is null)
{
g_keyboard = wsi_seat_get_keyboard(g_seat);
wsi_keyboard_add_listener(g_keyboard, &g_keyboardListener, null);
}
else if (!hasKeyboard && g_keyboard !is null)
{
wsi_keyboard_destroy(g_keyboard);
g_keyboard = null;
}
}
extern (C) void app.onSeatNameonSeatName(void* data, wl_seat* (parameter) wl_seat* ss, const(char)* name) nothrow @nogc {}
__gshared wl_seat_listener _error_ app.g_seatListenerg_seatListener = {&onSeatCapabilities, &onSeatName};
extern (C) void app.onGlobalonGlobal(void* data, wl_registry* reg, uint name,
const(char)* iface, uint ver) nothrow @nogc
{
static uint uint capped(uint advertised, uint want) nothrow @nogccapped(uint (parameter) uint advertisedadvertised, uint (parameter) uint wantwant) nothrow @nogc
{
return advertised < want ? advertised : want;
}
if (strcmp(iface, wl_compositor_interface.name) == 0)
g_compositor = cast(wl_compositor*) wsi_registry_bind(reg, name,
&wl_compositor_interface, capped(ver, 6));
else if (strcmp(iface, wl_shm_interface.name) == 0)
g_shm = cast(wl_shm*) wsi_registry_bind(reg, name, &wl_shm_interface, 1);
else if (strcmp(iface, xdg_wm_base_interface.name) == 0)
g_wmBase = cast(xdg_wm_base*) wsi_registry_bind(reg, name, &xdg_wm_base_interface, 1);
else if (strcmp(iface, wl_seat_interface.name) == 0 && g_seat is null)
{
g_seat = cast(wl_seat*) wsi_registry_bind(reg, name, &wl_seat_interface, capped(ver, 5));
wsi_seat_add_listener(g_seat, &g_seatListener, null);
}
else if (strcmp(iface, wl_data_device_manager_interface.name) == 0)
{
// v3 = dnd actions (set_actions / action / finish).
g_ddmVersion = capped(ver, 3);
g_ddm = cast(wl_data_device_manager*) wsi_registry_bind(reg, name,
&wl_data_device_manager_interface, g_ddmVersion);
}
}
extern (C) void app.onGlobalRemoveonGlobalRemove(void* data, wl_registry* reg, uint name) nothrow @nogc {}
__gshared wl_registry_listener _error_ app.g_registryListenerg_registryListener = {&onGlobal, &onGlobalRemove};
// ------------------------------------------------ window plumbing (scaffold)
bool app.ensureBufferensureBuffer(ref (struct) app.BufferBuffer (parameter) Buffer bb, int w, int h) nothrow @nogc
{
if (b.b.handlehandle !is null && (b.b.widthwidth != w || b.b.heightheight != h))
{
wsi_buffer_destroy(b.b.handlehandle);
munmap(b.b.pixelspixels, b.b.byteSizebyteSize);
b = Buffer.init;
}
if (b.b.handlehandle !is null)
return true;
immutable _error_ stridestride = w * 4;
immutable _error_ sizesize = cast((unresolved type) size_tsize_t) stride * h;
immutable _error_ fdfd = memfd_create("wsi-f16", MFD_CLOEXEC);
if (fd < 0 || ftruncate(fd, cast(long) size) != 0)
return false;
void* _error_ memmem = mmap(null, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mem is cast(void*)-1)
{
close(fd);
return false;
}
wl_shm_pool* _error_ poolpool = wsi_shm_create_pool(g_shm, fd, cast(int) size);
b.b.handlehandle = wsi_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888);
wsi_shm_pool_destroy(pool);
close(fd);
wsi_buffer_add_listener(b.b.handlehandle, &g_bufferListener, &b);
b.b.pixelspixels = cast(uint*) mem;
b.b.byteSizebyteSize = size;
b.b.widthwidth = w;
b.b.heightheight = h;
return true;
}
void void app.render(ulong idx) nothrow @nogcrender((alias) object.size_t = ulongsize_t (parameter) ulong idxidx) nothrow @nogc
{
(struct) app.SurfSurf* (local variable) _error_ ss = &g_surfs[idx];
if (s.(field) _error_ s.surfacesurface is null || !s.(field) _error_ s.configuredconfigured)
return;
(struct) app.BufferBuffer* (local variable) _error_ bufbuf = null;
foreach (ref (parameter) bb; s.(field) _error_ s.buffersbuffers)
if (!b.b.busybusy)
{
buf = &b;
break;
}
if (buf is null || !ensureBuffer(*buf, s.s.widthwidth, s.s.heightheight))
return;
immutable uint (local variable) immutable(uint) bgbg = (parameter) ulong idxidx == (constant) ulong app.IDX_A = 0LUIDX_A ? 0xff35506a : 0xff6a5035; // A blue, B amber
foreach ((parameter) yy; 0 .. buf.(field) _error_ buf.heightheight)
foreach ((parameter) xx; 0 .. buf.buf.widthwidth)
buf.buf.pixelspixels[cast((unresolved type) size_tsize_t) y * buf.buf.widthwidth + x] = bg;
wsi_surface_attach(s.s.surfacesurface, buf.buf.handlehandle, 0, 0);
wsi_surface_damage_buffer(s.s.surfacesurface, 0, 0, buf.buf.widthwidth, buf.buf.heightheight);
wsi_surface_commit(s.s.surfacesurface);
buf.buf.busybusy = true;
(__gshared global) int app.g_framesg_frames++;
}
extern (C) void app.onWmBasePingonWmBasePing(void* data, xdg_wm_base* (parameter) xdg_wm_base* bb, uint serial) nothrow @nogc
{
wsi_wm_base_pong(b, serial);
}
extern (C) void app.onToplevelConfigureonToplevelConfigure(void* data, xdg_toplevel* t, int w, int h,
wl_array* states) nothrow @nogc
{
(unresolved type) SurfSurf* _error_ ss = &g_surfs[cast((unresolved type) size_tsize_t) data];
s.s.pendingWpendingW = w;
s.s.pendingHpendingH = h;
}
extern (C) void app.onXdgSurfaceConfigureonXdgSurfaceConfigure(void* data, xdg_surface* (parameter) xdg_surface* ss, uint serial) nothrow @nogc
{
immutable _error_ idxidx = cast((unresolved type) size_tsize_t) data;
(unresolved type) SurfSurf* _error_ sfsf = &g_surfs[idx];
sf.sf.widthwidth = sf.sf.pendingWpendingW > 0 ? sf.sf.pendingWpendingW : winWidth;
sf.sf.heightheight = sf.sf.pendingHpendingH > 0 ? sf.sf.pendingHpendingH : winHeight;
wsi_xdg_surface_ack_configure(s, serial);
sf.sf.configuredconfigured = true;
render(idx);
if (g_firstPaintUs < 0 && g_surfs[IDX_A].g_surfs[IDX_A].configuredconfigured && g_surfs[IDX_B].g_surfs[IDX_B].configuredconfigured)
{
g_firstPaintUs = instrNowUs();
instrFirstConfigure();
}
}
extern (C) void app.onToplevelCloseonToplevelClose(void* data, xdg_toplevel* t) nothrow @nogc
{
instrCloseRequested();
g_running = false;
}
extern (C) void app.onToplevelConfigureBoundsonToplevelConfigureBounds(void* data, xdg_toplevel* t, int w, int h) nothrow @nogc {}
extern (C) void app.onToplevelWmCapabilitiesonToplevelWmCapabilities(void* data, xdg_toplevel* t, wl_array* caps) nothrow @nogc {}
extern (C) void app.onBufferReleaseonBufferRelease(void* data, wl_buffer* (parameter) wl_buffer* bb) nothrow @nogc
{
(cast((unresolved type) BufferBuffer*) data).(cast(Buffer*)data).busybusy = false;
}
__gshared xdg_wm_base_listener _error_ app.g_wmBaseListenerg_wmBaseListener = {&onWmBasePing};
__gshared xdg_surface_listener _error_ app.g_xdgSurfaceListenerg_xdgSurfaceListener = {&onXdgSurfaceConfigure};
__gshared xdg_toplevel_listener _error_ app.g_toplevelListenerg_toplevelListener = {
&onToplevelConfigure, &onToplevelClose,
&onToplevelConfigureBounds, &onToplevelWmCapabilities
};
__gshared wl_buffer_listener _error_ app.g_bufferListenerg_bufferListener = {&onBufferRelease};
void void app.makeWindow(ulong idx, const(char)* appId) nothrow @nogcmakeWindow((alias) object.size_t = ulongsize_t (parameter) ulong idxidx, const(char)* (parameter) const(char)* appIdappId) nothrow @nogc
{
(struct) app.SurfSurf* (local variable) _error_ ss = &g_surfs[idx];
s.s.surfacesurface = wsi_compositor_create_surface(g_compositor);
s.s.xdgSurfacexdgSurface = wsi_wm_base_get_xdg_surface(g_wmBase, s.s.surfacesurface);
wsi_xdg_surface_add_listener(s.s.xdgSurfacexdgSurface, &g_xdgSurfaceListener, cast(void*) idx);
s.s.topleveltoplevel = wsi_xdg_surface_get_toplevel(s.s.xdgSurfacexdgSurface);
wsi_toplevel_add_listener(s.s.topleveltoplevel, &g_toplevelListener, cast(void*) idx);
wsi_toplevel_set_title(s.s.topleveltoplevel, appId);
wsi_toplevel_set_app_id(s.s.topleveltoplevel, appId);
wsi_surface_commit(s.s.surfacesurface);
}
// --------------------------------------------------------------------- main
/// The startup probe: set_selection with serial=0 before ANY input event
/// exists anywhere on this connection. THE Wayland clipboard divergence:
/// without a recent input serial there is no clipboard write.
void void app.staleSelectionProbe() nothrow @nogcThe startup probe: set_selection with serial=0 before ANY input event
exists anywhere on this connection. THE Wayland clipboard divergence:
without a recent input serial there is no clipboard write.
staleSelectionProbe() nothrow @nogc
{
g_staleSource = makeTextSource(false);
wsi_data_device_set_selection(g_dataDevice, g_staleSource, 0);
wl_display_flush(g_display);
(__gshared global) bool app.g_staleProbeSentg_staleProbeSent = true;
instrEvent("set_selection", "serial=0 note=stale-no-input-event-exists");
}
int int D main(string[] args)main((alias) object.string = stringstring[] (parameter) string[] argsargs)
{
static int int app.main.parseInt(in char[] s) pure nothrow @nogc @safeparseInt(in char[] (parameter) const(char[]) ss) nothrow @nogc
{
int (local variable) int vv;
foreach ((parameter) const(char) chch; (parameter) const(char[]) ss)
if ((local variable) const(char) chch >= '0' && (local variable) const(char) chch <= '9')
(local variable) int vv = (local variable) int vv * 10 + ((local variable) const(char) chch - '0');
return (local variable) int vv;
}
if ((parameter) string[] argsargs.(field) ulong string[].lengthlength >= 2 && (parameter) string[] argsargs[1].(field) ulong string.lengthlength >= 4 && (parameter) string[] argsargs[1][0 .. 4] == "inje")
{
import inject : injectClick, injectDrag;
if ((parameter) string[] argsargs.(field) ulong string[].lengthlength == 6 && (parameter) string[] argsargs[2].(field) ulong string.lengthlength >= 2 && (parameter) string[] argsargs[2][0 .. 2] == "cl")
return injectClick(parseInt(args[3]), parseInt(args[4]), parseInt(args[5]));
if ((parameter) string[] argsargs.(field) ulong string[].lengthlength == 7 && (parameter) string[] argsargs[2].(field) ulong string.lengthlength >= 2 && (parameter) string[] argsargs[2][0 .. 2] == "dr")
return injectDrag(parseInt(args[3]), parseInt(args[4]),
parseInt(args[5]), parseInt(args[6]));
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("usage: inject click <x> <y> <btn> | inject drag <x1> <y1> <x2> <y2>\n");
return 2;
}
instrInit("f16_wayland");
const (local variable) const(char*) autoEnvautoEnv = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("WSI_AUTO_EXIT");
(__gshared global) bool app.g_autoExitg_autoExit = (local variable) const(char*) autoEnvautoEnv !is null && *(local variable) const(char*) autoEnvautoEnv == '1';
const (local variable) const(char*) runMsrunMs = char* core.stdc.stdlib.getenv(scope const(char*) name) nothrow @nogcgetenv("WSI_RUN_MS");
if ((local variable) const(char*) runMsrunMs !is null)
(__gshared global) long app.g_runUsCapg_runUsCap = int core.stdc.stdlib.atoi(scope const(char*) nptr) nothrow @nogcatoi((local variable) const(char*) runMsrunMs) * 1000L;
g_display = wl_display_connect(null);
if (g_display is null)
{
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("SKIP: no Wayland compositor (wl_display_connect returned null)\n");
return 0;
}
g_registry = wsi_display_get_registry(g_display);
wsi_registry_add_listener(g_registry, &g_registryListener, null);
wl_display_roundtrip(g_display); // globals
wl_display_roundtrip(g_display); // seat capabilities
if (g_compositor is null || g_shm is null || g_wmBase is null)
{
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("SKIP: compositor lacks a required global\n");
wl_display_disconnect(g_display);
return 0;
}
instrEvent("globals", "data_device_manager=%d version=%u seat=%d",
g_ddm !is null ? 1 : 0, g_ddmVersion, g_seat !is null ? 1 : 0);
if (g_ddm is null || g_seat is null)
{
// headless weston: wl_data_device_manager IS advertised, but with no
// seat there is no wl_data_device to bind it to — the clipboard is
// seat-scoped by construction. Registry probe is the Tier-A evidence.
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("ok: data_device_manager=%d but seat=%d; clipboard is seat-scoped, nothing to exercise\n",
g_ddm !is null ? 1 : 0, g_seat !is null ? 1 : 0);
wl_display_disconnect(g_display);
return 0;
}
g_dataDevice = wsi_ddm_get_data_device(g_ddm, g_seat);
wsi_data_device_add_listener(g_dataDevice, &g_dataDeviceListener, null);
wsi_wm_base_add_listener(g_wmBase, &g_wmBaseListener, null);
void app.makeWindow(ulong idx, const(char)* appId) nothrow @nogcmakeWindow((constant) ulong app.IDX_A = 0LUIDX_A, "wsi-f16-a");
void app.makeWindow(ulong idx, const(char)* appId) nothrow @nogcmakeWindow((constant) ulong app.IDX_B = 1LUIDX_B, "wsi-f16-b");
instrWindowCreated();
immutable (local variable) immutable(_error_) dispFddispFd = wl_display_get_fd(g_display);
while ((__gshared global) bool app.g_runningg_running)
{
while (wl_display_prepare_read(g_display) != 0)
wl_display_dispatch_pending(g_display);
wl_display_flush(g_display);
pollfd[3] (local variable) _error_ pfdspfds = [
pollfd(dispFd, POLLIN, 0),
pollfd(g_pipes[0].g_pipes[0].fdfd, POLLIN, 0),
pollfd(g_pipes[1].g_pipes[1].fdfd, POLLIN, 0),
];
if (poll(pfds.ptr, 3, 50) > 0 && (pfds[0].revents & POLLIN))
wl_display_read_events(g_display);
else
wl_display_cancel_read(g_display);
if (wl_display_dispatch_pending(g_display) == -1)
{
instrEvent("display_error", "errno=%d", wl_display_get_error(g_display));
break;
}
foreach ((local variable) int ii; 0 .. 2)
if (g_pipes[i].(field) _error_ (__error)[i].fdfd >= 0 && (pfds[i + 1].revents & (POLLIN | POLLHUP)))
void app.drainPipe(bool isDnd) nothrow @nogcdrainPipe((local variable) int ii == 1);
if (!(__gshared global) bool app.g_staleProbeSentg_staleProbeSent && (__gshared global) long app.g_firstPaintUsg_firstPaintUs >= 0
&& instrNowUs() > (__gshared global) long app.g_firstPaintUsg_firstPaintUs + 300_000)
void app.staleSelectionProbe() nothrow @nogcThe startup probe: set_selection with serial=0 before ANY input event
exists anywhere on this connection. THE Wayland clipboard divergence:
without a recent input serial there is no clipboard write.
staleSelectionProbe();
if ((__gshared global) long app.g_exitAtUsg_exitAtUs >= 0 && instrNowUs() > (__gshared global) long app.g_exitAtUsg_exitAtUs)
(__gshared global) bool app.g_runningg_running = false;
if ((__gshared global) bool app.g_autoExitg_autoExit && instrNowUs() > (__gshared global) long app.g_runUsCapg_runUsCap)
(__gshared global) bool app.g_runningg_running = false;
}
instrEvent("summary", "dnd_finished=%d dnd_motions=%d sel_source_alive=%d",
g_dndFinished ? 1 : 0, g_dndMotions, g_selSource !is null ? 1 : 0);
// Teardown: children before parents.
foreach (ref (parameter) pp; g_pipes)
if (p.p.fdfd >= 0)
close(p.p.fdfd);
if (g_dragSource !is null)
wsi_data_source_destroy(g_dragSource);
if (g_selSource !is null)
wsi_data_source_destroy(g_selSource);
if (g_staleSource !is null)
wsi_data_source_destroy(g_staleSource);
if (g_dataDevice !is null)
wsi_data_device_release(g_dataDevice);
if (g_ddm !is null)
wsi_ddm_destroy(g_ddm);
foreach ((parameter) idxidx; 0 .. g_surfs.(field) _error_ g_surfs.lengthlength)
{
(unresolved type) SurfSurf* _error_ ss = &g_surfs[idx];
foreach (ref (parameter) bb; s.s.buffersbuffers)
if (b.b.handlehandle !is null)
{
wsi_buffer_destroy(b.b.handlehandle);
munmap(b.b.pixelspixels, b.b.byteSizebyteSize);
}
wsi_toplevel_destroy(s.s.topleveltoplevel);
wsi_xdg_surface_destroy(s.s.xdgSurfacexdgSurface);
wsi_surface_destroy(s.s.surfacesurface);
}
if (g_keyboard !is null)
wsi_keyboard_destroy(g_keyboard);
if (g_pointer !is null)
wsi_pointer_destroy(g_pointer);
if (g_seat !is null)
wsi_seat_destroy(g_seat);
wsi_wm_base_destroy(g_wmBase);
wl_proxy_destroy(cast(wl_proxy*) g_shm);
wl_proxy_destroy(cast(wl_proxy*) g_compositor);
wl_proxy_destroy(cast(wl_proxy*) g_registry);
wl_display_disconnect(g_display);
int core.stdc.stdio.printf(scope const(char*) format, scope const ...) nothrow @nogcprintf("ok: dnd_finished=%d dnd_motions=%d\n", (__gshared global) bool app.g_dndFinishedg_dndFinished ? 1 : 0, (__gshared global) int app.g_dndMotionsg_dndMotions);
return 0;
}