// F07 IME / text input demo — an editable line with a visible caret, driven by
// zwp_text_input_v3 (text-input-unstable-v3) on top of the xdg-shell scaffold
// (../scaffold/app.d; findings: ../../scaffold.md, ../../f07-text-input.md).
//
// What it implements, per the protocol XML (text-input-unstable-v3.xml):
//
// - bind zwp_text_input_manager_v3, get_text_input for the seat;
// - on zwp_text_input_v3.enter: enable + set_surrounding_text +
// set_content_type + set_cursor_rectangle(caret cells) + commit;
// - the v3 double-buffered event state: preedit_string / commit_string /
// delete_surrounding_text only latch *pending* state, which is applied
// atomically on done(serial) in the exact order the XML mandates
// (1. drop old preedit, 2. delete surrounding, 3. insert commit string,
// 4. recalculate surrounding, 5-6. install new preedit + caret);
// - the serial discipline: the client counts its own commit requests; a
// done.serial equal to that count permits new state requests, a stale
// serial means "apply the edits, but do not touch protocol state yet".
//
// The line is rendered with block glyphs (no font lib): each committed UTF-8
// byte is a colored cell (color = hash of the byte), the pre-edit is rendered
// inline at the caret as cells over a bright underline band, and the caret is
// a white bar. The *real* strings are logged (`text committed="…" preedit="…"`).
// wl_keyboard events are logged in full (keycode, xkb keysym, UTF-8) so the
// findings doc can answer which key events still arrive while an IME composes.
// Plain typing also works without an IME: xkbcommon translates the key, the
// demo edits locally and resends state with change_cause=other.
//
// Headless-safe: no compositor → SKIP, exit 0; no zwp_text_input_manager_v3 or
// no wl_seat → the absence is logged as the finding and the run still exits 0.
module (module) appapp;
import c; // ImportC: wayland-client + xdg-shell + text-input-v3 glue + xkbcommon
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, (alias) app.snprintf = int core.stdc.stdio.snprintf(scope char* s, ulong n, scope const(char*) format, scope const ...) nothrow @nogcsnprintf;
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.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.memcpy = void* core.stdc.string.memcpy(return scope void* s1, scope const(void*) s2, ulong n) pure nothrow @nogcmemcpy, (alias) app.memmove = void* core.stdc.string.memmove(return scope void* s1, scope const(void*) s2, ulong n) pure nothrow @nogcmemmove, (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;
// ----------------------------------------------------------------- tunables
enum int (constant) int app.defaultWidth = 640defaultWidth = 640;
enum int (constant) int app.defaultHeight = 220defaultHeight = 220;
enum int (constant) int app.autoExitFrames = 150autoExitFrames = 150; // ≈ 2.5 s at 60 Hz
enum long (constant) long app.autoExitUsCap = 3000000LautoExitUsCap = 3_000_000;
enum int (constant) int app.textCap = 240textCap = 240; // committed-line byte budget (protocol max is 4000)
// Block-glyph layout (surface-local px — the units set_cursor_rectangle takes).
enum int (constant) int app.cellX0 = 16cellX0 = 16, (constant) int app.cellY0 = 96cellY0 = 96, (constant) int app.cellW = 12cellW = 12, (constant) int app.cellH = 48cellH = 48, (constant) int app.underlineH = 6underlineH = 6;
// Keysyms used for local editing (values from xkbcommon-keysyms.h).
enum uint (constant) uint app.keyBackSpace = 65288ukeyBackSpace = 0xff08, (constant) uint app.keyEscape = 65307ukeyEscape = 0xff1b, (constant) uint app.keyLeft = 65361ukeyLeft = 0xff51, (constant) uint app.keyRight = 65363ukeyRight = 0xff53;
// -------------------------------------------------------------------- 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;
}
__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;
wl_seat* _error_ app.g_seatg_seat;
wl_keyboard* _error_ app.g_keyboardg_keyboard;
xdg_wm_base* _error_ app.g_wmBaseg_wmBase;
wl_surface* _error_ app.g_surfaceg_surface;
xdg_surface* _error_ app.g_xdgSurfaceg_xdgSurface;
xdg_toplevel* _error_ app.g_toplevelg_toplevel;
wl_callback* _error_ app.g_frameCbg_frameCb;
zwp_text_input_manager_v3* _error_ app.g_tiManagerg_tiManager;
zwp_text_input_v3* _error_ app.g_tig_ti;
xkb_context* _error_ app.g_xkbCtxg_xkbCtx;
xkb_keymap* _error_ app.g_xkbKeymapg_xkbKeymap;
xkb_state* _error_ app.g_xkbStateg_xkbState;
(struct) app.BufferBuffer[2] _error_ app.g_buffersg_buffers;
int (__gshared global) int app.g_widthg_width = defaultWidth, (__gshared global) int app.g_heightg_height = defaultHeight;
int (__gshared global) int app.g_pendingWidthg_pendingWidth, (__gshared global) int app.g_pendingHeightg_pendingHeight;
bool (__gshared global) bool app.g_configuredg_configured, (__gshared global) bool app.g_runningg_running = true, (__gshared global) bool app.g_autoExitg_autoExit;
int (__gshared global) int app.g_framesg_frames, (__gshared global) int app.g_commitsg_commits;
// --- the editable line -------------------------------------------------
char[(constant) int app.textCap = 240textCap + 1] (__gshared global) char[241] app.g_textg_text = '\0'; // committed text, NUL-terminated
int (__gshared global) int app.g_textLeng_textLen;
int (__gshared global) int app.g_cursorg_cursor; // byte index into g_text
char[textCap + 1] (__gshared global) char[241] app.g_preeditg_preedit = '\0'; // current (applied) pre-edit
int (__gshared global) int app.g_preeditLeng_preeditLen;
int (__gshared global) int app.g_preeditCbg_preeditCb, (__gshared global) int app.g_preeditCeg_preeditCe; // caret begin/end inside the pre-edit, bytes
// --- zwp_text_input_v3 client state ------------------------------------
bool (__gshared global) bool app.g_tiFocusedg_tiFocused; // between enter and leave
uint (__gshared global) uint app.g_tiCommitsg_tiCommits; // number of commit requests sent == expected done serial
// pending (double-buffered) event state, applied on done():
char[textCap + 1] (__gshared global) char[241] app.g_pendPreeditg_pendPreedit = '\0';
int (__gshared global) int app.g_pendPreeditLeng_pendPreeditLen, (__gshared global) int app.g_pendPreeditCbg_pendPreeditCb, (__gshared global) int app.g_pendPreeditCeg_pendPreeditCe;
char[textCap + 1] (__gshared global) char[241] app.g_pendCommitg_pendCommit = '\0';
int (__gshared global) int app.g_pendCommitLeng_pendCommitLen;
uint (__gshared global) uint app.g_pendDelBeforeg_pendDelBefore, (__gshared global) uint app.g_pendDelAfterg_pendDelAfter;
}
// ----------------------------------------------------------- text utilities
int int app.clampInt(int v, int lo, int hi) nothrow @nogcclampInt(int (parameter) int vv, int (parameter) int lolo, int (parameter) int hihi) nothrow @nogc
{
return (parameter) int vv < (parameter) int lolo ? (parameter) int lolo : ((parameter) int vv > (parameter) int hihi ? (parameter) int hihi : (parameter) int vv);
}
/// Copy a NUL-terminated C string into a bounded buffer, returning the length.
int int app.copyStr(const(char)* src, ref char[241] dst) nothrow @nogcCopy a NUL-terminated C string into a bounded buffer, returning the length.
copyStr(const(char)* (parameter) const(char)* srcsrc, ref char[textCap + 1] (parameter) char[241] dstdst) nothrow @nogc
{
int (local variable) int nn = (parameter) const(char)* srcsrc is null ? 0 : cast(int) ulong core.stdc.string.strlen(scope const(char*) s) pure nothrow @nogcstrlen((parameter) const(char)* srcsrc);
if ((local variable) int nn > (constant) int app.textCap = 240textCap)
(local variable) int nn = (constant) int app.textCap = 240textCap;
if ((local variable) int nn > 0)
void* core.stdc.string.memcpy(return scope void* s1, scope const(void*) s2, ulong n) pure nothrow @nogcmemcpy((parameter) char[241] dstdst.(constant) char* char[241].ptr = &dstptr, (parameter) const(char)* srcsrc, (local variable) int nn);
(parameter) char[241] dstdst[(local variable) int nn] = '\0';
return (local variable) int nn;
}
void void app.insertAtCursor(const(char)* s, int n) nothrow @nogcinsertAtCursor(const(char)* (parameter) const(char)* ss, int (parameter) int nn) nothrow @nogc
{
if ((parameter) int nn <= 0 || (__gshared global) int app.g_textLeng_textLen + (parameter) int nn > (constant) int app.textCap = 240textCap)
return;
void* core.stdc.string.memmove(return scope void* s1, scope const(void*) s2, ulong n) pure nothrow @nogcmemmove((__gshared global) char[241] app.g_textg_text.(constant) char* char[241].ptr = &g_textptr + (__gshared global) int app.g_cursorg_cursor + (parameter) int nn, (__gshared global) char[241] app.g_textg_text.(constant) char* char[241].ptr = &g_textptr + (__gshared global) int app.g_cursorg_cursor, (__gshared global) int app.g_textLeng_textLen - (__gshared global) int app.g_cursorg_cursor);
void* core.stdc.string.memcpy(return scope void* s1, scope const(void*) s2, ulong n) pure nothrow @nogcmemcpy((__gshared global) char[241] app.g_textg_text.(constant) char* char[241].ptr = &g_textptr + (__gshared global) int app.g_cursorg_cursor, (parameter) const(char)* ss, (parameter) int nn);
(__gshared global) int app.g_textLeng_textLen += (parameter) int nn;
(__gshared global) int app.g_cursorg_cursor += (parameter) int nn;
(__gshared global) char[241] app.g_textg_text[(__gshared global) int app.g_textLeng_textLen] = '\0';
}
void void app.deleteRange(int begin, int end) nothrow @nogcdeleteRange(int (parameter) int beginbegin, int (parameter) int endend) nothrow @nogc // [begin, end) in bytes
{
(parameter) int beginbegin = int app.clampInt(int v, int lo, int hi) nothrow @nogcclampInt((parameter) int beginbegin, 0, (__gshared global) int app.g_textLeng_textLen);
(parameter) int endend = int app.clampInt(int v, int lo, int hi) nothrow @nogcclampInt((parameter) int endend, (parameter) int beginbegin, (__gshared global) int app.g_textLeng_textLen);
void* core.stdc.string.memmove(return scope void* s1, scope const(void*) s2, ulong n) pure nothrow @nogcmemmove((__gshared global) char[241] app.g_textg_text.(constant) char* char[241].ptr = &g_textptr + (parameter) int beginbegin, (__gshared global) char[241] app.g_textg_text.(constant) char* char[241].ptr = &g_textptr + (parameter) int endend, (__gshared global) int app.g_textLeng_textLen - (parameter) int endend);
(__gshared global) int app.g_textLeng_textLen -= (parameter) int endend - (parameter) int beginbegin;
(__gshared global) int app.g_cursorg_cursor = (__gshared global) int app.g_cursorg_cursor >= (parameter) int endend ? (__gshared global) int app.g_cursorg_cursor - ((parameter) int endend - (parameter) int beginbegin) : int app.clampInt(int v, int lo, int hi) nothrow @nogcclampInt((__gshared global) int app.g_cursorg_cursor, 0, (parameter) int beginbegin);
(__gshared global) char[241] app.g_textg_text[(__gshared global) int app.g_textLeng_textLen] = '\0';
}
/// Step one UTF-8 code point left/right from `idx` (indices never split a
/// code point — the same invariant text-input-v3 demands of its byte offsets).
int int app.cpPrev(int idx) nothrow @nogcStep one UTF-8 code point left/right from idx (indices never split a
code point — the same invariant text-input-v3 demands of its byte offsets).
cpPrev(int (parameter) int idxidx) nothrow @nogc
{
do
--(parameter) int idxidx;
while ((parameter) int idxidx > 0 && ((__gshared global) char[241] app.g_textg_text[(parameter) int idxidx] & 0xc0) == 0x80);
return (parameter) int idxidx < 0 ? 0 : (parameter) int idxidx;
}
int int app.cpNext(int idx) nothrow @nogccpNext(int (parameter) int idxidx) nothrow @nogc
{
do
++(parameter) int idxidx;
while ((parameter) int idxidx < (__gshared global) int app.g_textLeng_textLen && ((__gshared global) char[241] app.g_textg_text[(parameter) int idxidx] & 0xc0) == 0x80);
return (parameter) int idxidx > (__gshared global) int app.g_textLeng_textLen ? (__gshared global) int app.g_textLeng_textLen : (parameter) int idxidx;
}
void void app.logText() nothrow @nogclogText() nothrow @nogc
{
instrEvent("text", "committed=\"%s\" cursor=%d preedit=\"%s\" preedit_cursor=%d..%d",
g_text.g_text.ptrptr, g_cursor, g_preedit.g_preedit.ptrptr, g_preeditCb, g_preeditCe);
}
// --------------------------------------------- caret rect + protocol commits
/// Caret rectangle in surface-local coordinates: the cell the caret occupies,
/// accounting for the pre-edit caret offset (cursor_begin) when composing.
void void app.caretRect(out int x, out int y, out int w, out int h) nothrow @nogcCaret rectangle in surface-local coordinates: the cell the caret occupies,
accounting for the pre-edit caret offset (cursor_begin) when composing.
caretRect(out int (parameter) int xx, out int (parameter) int yy, out int (parameter) int ww, out int (parameter) int hh) nothrow @nogc
{
immutable (local variable) immutable(int) colcol = (__gshared global) int app.g_cursorg_cursor + ((__gshared global) int app.g_preeditLeng_preeditLen > 0 ? int app.clampInt(int v, int lo, int hi) nothrow @nogcclampInt((__gshared global) int app.g_preeditCbg_preeditCb, 0, (__gshared global) int app.g_preeditLeng_preeditLen) : 0);
(parameter) int xx = (constant) int app.cellX0 = 16cellX0 + (local variable) immutable(int) colcol * (constant) int app.cellW = 12cellW;
(parameter) int yy = (constant) int app.cellY0 = 96cellY0;
(parameter) int ww = (constant) int app.cellW = 12cellW;
(parameter) int hh = (constant) int app.cellH = 48cellH;
}
/// Send the full client state and commit it — the only place a
/// zwp_text_input_v3.commit is issued, so g_tiCommits *is* the serial the
/// protocol's done event must echo. cause: 0=input_method, 1=other, -1=omit.
void void app.tiCommitState(bool enable, int cause) nothrow @nogcSend the full client state and commit it — the only place a
zwp_text_input_v3.commit is issued, so g_tiCommits is the serial the
protocol's done event must echo. cause: 0=input_method, 1=other, -1=omit.
tiCommitState(bool (parameter) bool enableenable, int (parameter) int causecause) nothrow @nogc
{
if (g_ti is null || (!(parameter) bool enableenable && !(__gshared global) bool app.g_tiFocusedg_tiFocused))
return;
if ((parameter) bool enableenable)
wsi_text_input_enable(g_ti);
wsi_text_input_set_surrounding_text(g_ti, g_text.g_text.ptrptr, g_cursor, g_cursor);
if ((parameter) int causecause >= 0)
wsi_text_input_set_text_change_cause(g_ti, cast(uint) cause);
wsi_text_input_set_content_type(g_ti,
ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE, ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL);
int (local variable) int xx, (local variable) int yy, (local variable) int ww, (local variable) int hh;
void app.caretRect(out int x, out int y, out int w, out int h) nothrow @nogcCaret rectangle in surface-local coordinates: the cell the caret occupies,
accounting for the pre-edit caret offset (cursor_begin) when composing.
caretRect((local variable) int xx, (local variable) int yy, (local variable) int ww, (local variable) int hh);
wsi_text_input_set_cursor_rectangle(g_ti, x, y, w, h);
wsi_text_input_commit(g_ti);
(__gshared global) uint app.g_tiCommitsg_tiCommits++;
instrEvent("ti_commit_state", "serial=%u enable=%d cause=%d cursor_rect=%d,%d %dx%d",
g_tiCommits, enable ? 1 : 0, cause, x, y, w, h);
}
// ------------------------------------------------------------ shm + drawing
bool app.ensureBufferensureBuffer(ref (struct) app.BufferBuffer (parameter) Buffer bb, int (parameter) int ww, int (parameter) int hh) 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-f07", 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 app.fillRectfillRect(ref (struct) app.BufferBuffer (parameter) Buffer bb, int (parameter) int xx, int (parameter) int yy, int (parameter) int ww, int (parameter) int hh, uint argb) nothrow @nogc
{
immutable _error_ x0x0 = clampInt(x, 0, b.b.widthwidth), _error_ x1x1 = clampInt(x + w, 0, b.b.widthwidth);
immutable _error_ y0y0 = clampInt(y, 0, b.b.heightheight), _error_ y1y1 = clampInt(y + h, 0, b.b.heightheight);
foreach ((parameter) yyyy; y0 .. y1)
{
uint* _error_ rowrow = b.b.pixelspixels + cast((unresolved type) size_tsize_t) yy * b.b.widthwidth;
row[x0 .. x1] = argb;
}
}
/// Block glyph: a byte becomes a solid cell whose color hashes the byte value,
/// so distinct characters are visually distinct without any font machinery.
uint uint app.cellColor(ubyte c, bool preedit) nothrow @nogcBlock glyph: a byte becomes a solid cell whose color hashes the byte value,
so distinct characters are visually distinct without any font machinery.
cellColor(ubyte (parameter) ubyte cc, bool (parameter) bool preeditpreedit) nothrow @nogc
{
immutable uint (local variable) immutable(uint) hh = ((parameter) ubyte cc * 2654435761u) >> 8;
immutable uint (local variable) immutable(uint) rr = 0x50 + ((local variable) immutable(uint) hh & 0x7f), (local variable) immutable(uint) gg = 0x50 + (((local variable) immutable(uint) hh >> 7) & 0x7f),
(local variable) immutable(uint) bchbch = 0x50 + (((local variable) immutable(uint) hh >> 14) & 0x7f);
// pre-edit cells are dimmed toward blue so they read as "not committed yet"
return (parameter) bool preeditpreedit ? 0xff000000 | ((local variable) immutable(uint) rr / 2 << 16) | ((local variable) immutable(uint) gg / 2 << 8) | 0xff
: 0xff000000 | ((local variable) immutable(uint) rr << 16) | ((local variable) immutable(uint) gg << 8) | (local variable) immutable(uint) bchbch;
}
void app.paintpaint(ref (struct) app.BufferBuffer (parameter) Buffer bb) nothrow @nogc
{
fillRect(b, 0, 0, b.b.widthwidth, b.b.heightheight, 0xff14181c); // background
// status blocks (the on-screen debug string, block-glyph form): focus,
// text-input present, pre-edit active, then the done-serial low bits.
fillRect(b, cellX0, 16, 24, 24, g_tiFocused ? 0xff30c030 : 0xffc03030);
fillRect(b, cellX0 + 32, 16, 24, 24, g_ti !is null ? 0xff3060e0 : 0xff606060);
fillRect(b, cellX0 + 64, 16, 24, 24, g_preeditLen > 0 ? 0xffe0c020 : 0xff606060);
foreach ((parameter) bitbit; 0 .. 8)
fillRect(b, cellX0 + 112 + bit * 16, 16, 12, 24,
(g_tiCommits >> (7 - bit)) & 1 ? 0xffe0e0e0 : 0xff404040);
fillRect(b, cellX0 - 4, cellY0 - 4, b.b.widthwidth - 2 * cellX0 + 8, cellH + 8, 0xff20262c); // field
// committed text before the caret, pre-edit, committed after — inline.
int _error_ colcol = 0;
void void cells(const(char)* s, int begin, int end, bool pre) nothrow @nogccells(const(char)* (parameter) const(char)* ss, int (parameter) int beginbegin, int (parameter) int endend, bool (parameter) bool prepre) nothrow @nogc
{
foreach ((parameter) ii; begin .. end)
{
fillRect(b, cellX0 + col * cellW + 1, cellY0, cellW - 2, cellH - underlineH - 2,
cellColor(cast(ubyte) s[i], pre));
if (pre)
fillRect(b, cellX0 + col * cellW, cellY0 + cellH - underlineH,
cellW, underlineH, 0xffffd040); // the underline band
col++;
}
}
cells(g_text.g_text.ptrptr, 0, g_cursor, false);
cells(g_preedit.g_preedit.ptrptr, 0, g_preeditLen, true);
cells(g_text.g_text.ptrptr, g_cursor, g_textLen, false);
int _error_ cxcx, _error_ cycy, _error_ cwcw, _error_ chch;
caretRect(cx, cy, cw, ch);
fillRect(b, cx - 1, cy - 4, 3, ch + 8, 0xffffffff); // the caret bar
}
void void app.render() nothrow @nogcrender() nothrow @nogc
{
(struct) app.BufferBuffer* (local variable) _error_ bufbuf = null;
foreach (ref (parameter) bb; g_buffers)
if (!b.b.busybusy)
{
buf = &b;
break;
}
if (buf is null || !ensureBuffer(*buf, g_width, g_height))
return;
paint(*buf);
wsi_surface_attach(g_surface, buf.buf.handlehandle, 0, 0);
wsi_surface_damage_buffer(g_surface, 0, 0, buf.buf.widthwidth, buf.buf.heightheight);
if (g_frameCb is null)
{
g_frameCb = wsi_surface_frame(g_surface);
wsi_callback_add_listener(g_frameCb, &g_frameListener, null);
}
wsi_surface_commit(g_surface);
buf.buf.busybusy = true;
(__gshared global) int app.g_commitsg_commits++;
}
// ----------------------------------------------- zwp_text_input_v3 listeners
extern (C) void app.onTiEnteronTiEnter(void* data, zwp_text_input_v3* ti, wl_surface* (parameter) wl_surface* ss) nothrow @nogc
{
g_tiFocused = true;
instrEvent("ti_enter");
// Per the XML: "After an enter event or disable request all state
// information is invalidated and needs to be resent by the client."
tiCommitState(true, -1);
}
extern (C) void app.onTiLeaveonTiLeave(void* data, zwp_text_input_v3* ti, wl_surface* (parameter) wl_surface* ss) nothrow @nogc
{
// "The client should reset any preedit string previously set" — the
// composition simply evaporates; nothing is committed.
immutable _error_ hadPreedithadPreedit = g_preeditLen;
g_preeditLen = 0;
g_preedit[0] = '\0';
instrEvent("ti_leave", "dropped_preedit_bytes=%d", hadPreedit);
wsi_text_input_disable(g_ti);
wsi_text_input_commit(g_ti);
g_tiCommits++;
g_tiFocused = false;
instrEvent("ti_commit_state", "serial=%u enable=0 disable=1", g_tiCommits);
logText();
}
extern (C) void app.onTiPreeditonTiPreedit(void* data, zwp_text_input_v3* ti,
const(char)* text, int cursorBegin, int cursorEnd) nothrow @nogc
{
// Double-buffered: latch only; applied on done().
g_pendPreeditLen = copyStr(text, g_pendPreedit);
g_pendPreeditCb = cursorBegin;
g_pendPreeditCe = cursorEnd;
instrEvent("ti_preedit_string", "text=\"%s\" cursor_begin=%d cursor_end=%d (pending)",
g_pendPreedit.g_pendPreedit.ptrptr, cursorBegin, cursorEnd);
}
extern (C) void app.onTiCommitStringonTiCommitString(void* data, zwp_text_input_v3* ti, const(char)* text) nothrow @nogc
{
g_pendCommitLen = copyStr(text, g_pendCommit);
instrEvent("ti_commit_string", "text=\"%s\" (pending)", g_pendCommit.g_pendCommit.ptrptr);
}
extern (C) void app.onTiDeleteSurroundingonTiDeleteSurrounding(void* data, zwp_text_input_v3* ti,
uint beforeLength, uint afterLength) nothrow @nogc
{
g_pendDelBefore = beforeLength;
g_pendDelAfter = afterLength;
instrEvent("ti_delete_surrounding_text", "before=%u after=%u (pending)",
beforeLength, afterLength);
}
/// done(serial): atomically apply the pending state in the order the protocol
/// XML mandates, then — only if the serial matches our commit count — answer
/// with refreshed surrounding text / cursor rectangle.
extern (C) void app.onTiDonedone(serial): atomically apply the pending state in the order the protocol
XML mandates, then — only if the serial matches our commit count — answer
with refreshed surrounding text / cursor rectangle.
onTiDone(void* data, zwp_text_input_v3* ti, uint serial) nothrow @nogc
{
immutable _error_ inSyncinSync = serial == g_tiCommits;
instrEvent("ti_done", "serial=%u client_commits=%u in_sync=%d", serial, g_tiCommits, inSync);
bool _error_ surroundingChangedsurroundingChanged = false;
// 1. Replace existing preedit string with the cursor.
g_preeditLen = 0;
g_preedit[0] = '\0';
// 2. Delete requested surrounding text (lengths are bytes around the cursor).
if (g_pendDelBefore != 0 || g_pendDelAfter != 0)
{
deleteRange(g_cursor - cast(int) g_pendDelBefore, g_cursor + cast(int) g_pendDelAfter);
surroundingChanged = true;
}
// 3. Insert commit string with the cursor at its end.
if (g_pendCommitLen > 0)
{
insertAtCursor(g_pendCommit.g_pendCommit.ptrptr, g_pendCommitLen);
surroundingChanged = true;
}
// 4. (surrounding text is recalculated when we resend state below)
// 5./6. Insert new preedit text at the cursor, with the caret inside it.
g_preeditLen = g_pendPreeditLen;
memcpy(g_preedit.g_preedit.ptrptr, g_pendPreedit.g_pendPreedit.ptrptr, g_pendPreeditLen + 1);
g_preeditCb = g_pendPreeditCb;
g_preeditCe = g_pendPreeditCe;
// Pending state resets to initial after every done (per the XML).
g_pendPreeditLen = g_pendPreeditCb = g_pendPreeditCe = 0;
g_pendPreedit[0] = '\0';
g_pendCommitLen = 0;
g_pendCommit[0] = '\0';
g_pendDelBefore = g_pendDelAfter = 0;
logText();
// Serial discipline: a stale serial means our state is already newer than
// what the IM saw — apply the edits but send nothing until it catches up.
if (inSync && (surroundingChanged || g_preeditLen > 0))
tiCommitState(false, surroundingChanged ? 0 : -1); // 0 = input_method
}
// ----------------------------------------------------- wl_keyboard listeners
// Logged exhaustively: the F07 finding "which raw key events still arrive
// while the IME composes" falls straight out of this stream.
extern (C) void app.onKbKeymaponKbKeymap(void* data, wl_keyboard* kb, uint format, int (parameter) int fdfd, uint (parameter) uint sizesize) nothrow @nogc
{
instrEvent("kb_keymap", "format=%u size=%u", format, size);
if (format == WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1 && g_xkbCtx !is null)
{
void* _error_ memmem = mmap(null, size, PROT_READ, MAP_PRIVATE, fd, 0);
if (mem !is cast(void*)-1)
{
g_xkbKeymap = xkb_keymap_new_from_string(g_xkbCtx, cast(const(char)*) mem,
XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS);
if (g_xkbKeymap !is null)
g_xkbState = xkb_state_new(g_xkbKeymap);
munmap(mem, size);
}
}
close(fd);
}
extern (C) void app.onKbEnteronKbEnter(void* data, wl_keyboard* kb, uint serial, wl_surface* (parameter) wl_surface* ss,
wl_array* keys) nothrow @nogc
{
instrEvent("kb_enter", "serial=%u pressed_keys=%zu", serial, keys.keys.sizesize / 4);
}
extern (C) void app.onKbLeaveonKbLeave(void* data, wl_keyboard* kb, uint serial, wl_surface* (parameter) wl_surface* ss) nothrow @nogc
{
instrEvent("kb_leave", "serial=%u", serial);
}
extern (C) void app.onKbKeyonKbKey(void* data, wl_keyboard* kb, uint serial, uint time,
uint key, uint state) nothrow @nogc
{
char[64] _error_ symNamesymName = '\0';
char[8] _error_ utf8utf8 = '\0';
uint _error_ symsym = 0;
int _error_ utf8Lenutf8Len = 0;
if (g_xkbState !is null)
{
immutable _error_ keycodekeycode = key + 8; // evdev → XKB keycode offset
sym = xkb_state_key_get_one_sym(g_xkbState, keycode);
xkb_keysym_get_name(sym, symName.symName.ptrptr, symName.length);
utf8Len = xkb_state_key_get_utf8(g_xkbState, keycode, utf8.utf8.ptrptr, utf8.length);
}
instrEvent("key", "serial=%u time=%u keycode=%u state=%s sym=0x%x name=%s utf8=\"%s\"",
serial, time, key, state == 1 ? "pressed"."pressed".ptrptr : "released"."released".ptrptr,
sym, symName.symName.ptrptr, utf8.utf8.ptrptr);
if (state != 1) // act on presses only
return;
// Local (non-IME) editing path: change_cause=other on the resulting state.
if (sym == keyBackSpace && g_cursor > 0)
deleteRange(cpPrev(g_cursor), g_cursor);
else if (sym == keyLeft)
g_cursor = g_cursor > 0 ? cpPrev(g_cursor) : 0;
else if (sym == keyRight)
g_cursor = cpNext(g_cursor);
else if (sym == keyEscape)
{
g_running = false;
return;
}
else if (utf8Len > 0 && cast(ubyte) utf8[0] >= 0x20 && utf8[0] != 0x7f)
insertAtCursor(utf8.utf8.ptrptr, utf8Len);
else
return;
logText();
tiCommitState(false, 1); // 1 = change_cause other (keyboard, not the IM)
}
extern (C) void app.onKbModifiersonKbModifiers(void* data, wl_keyboard* kb, uint serial,
uint depressed, uint latched, uint locked, uint group) nothrow @nogc
{
if (g_xkbState !is null)
xkb_state_update_mask(g_xkbState, depressed, latched, locked, 0, 0, group);
instrEvent("kb_modifiers", "depressed=0x%x latched=0x%x locked=0x%x group=%u",
depressed, latched, locked, group);
}
extern (C) void app.onKbRepeatInfoonKbRepeatInfo(void* data, wl_keyboard* kb, int rate, int delay) nothrow @nogc
{
instrEvent("kb_repeat_info", "rate=%d delay=%d", rate, delay);
}
// --------------------------------------------------------- shell + bookkeeping
extern (C) void app.onGlobalonGlobal(void* data, wl_registry* reg, uint name,
const(char)* iface, uint ver) nothrow @nogc
{
instrEvent("global", "iface=%s version=%u", iface, ver); // the registry dump
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, 4));
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 = cast(wl_seat*) wsi_registry_bind(reg, name, &wl_seat_interface, capped(ver, 4));
else if (strcmp(iface, zwp_text_input_manager_v3_interface.name) == 0)
g_tiManager = cast(zwp_text_input_manager_v3*) wsi_registry_bind(reg, name,
&zwp_text_input_manager_v3_interface, 1);
}
extern (C) void app.onGlobalRemoveonGlobalRemove(void* data, wl_registry* reg, uint name) nothrow @nogc
{
}
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.onSeatCapabilitiesonSeatCapabilities(void* data, wl_seat* (parameter) wl_seat* ss, uint caps) nothrow @nogc
{
instrEvent("seat_capabilities", "caps=0x%x", caps);
enum uint (constant) uint capKeyboard = 2capKeyboard = 2; // WL_SEAT_CAPABILITY_KEYBOARD
if ((caps & capKeyboard) && g_keyboard is null)
{
g_keyboard = wsi_seat_get_keyboard(s);
wsi_keyboard_add_listener(g_keyboard, &g_keyboardListener, null);
instrStep("wl_seat_get_keyboard");
}
}
extern (C) void app.onSeatNameonSeatName(void* data, wl_seat* (parameter) wl_seat* ss, const(char)* name) nothrow @nogc
{
instrEvent("seat_name", "name=%s", name);
}
extern (C) void app.onToplevelConfigureonToplevelConfigure(void* data, xdg_toplevel* t, int (parameter) int ww, int (parameter) int hh,
wl_array* states) nothrow @nogc
{
g_pendingWidth = w;
g_pendingHeight = h;
}
extern (C) void app.onXdgSurfaceConfigureonXdgSurfaceConfigure(void* data, xdg_surface* (parameter) xdg_surface* ss, uint serial) nothrow @nogc
{
g_width = g_pendingWidth > 0 ? g_pendingWidth : defaultWidth;
g_height = g_pendingHeight > 0 ? g_pendingHeight : defaultHeight;
wsi_xdg_surface_ack_configure(s, serial);
if (!g_configured)
{
g_configured = true;
instrFirstConfigure();
}
render();
}
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 (parameter) int ww, int (parameter) int hh) 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;
// sway/wlroots holds shm buffers until the *next* commit replaces them, so
// both buffers can be busy when a frame callback fires; recover here.
if (g_running && g_configured && g_frameCb is null)
render();
}
extern (C) void app.onFrameDoneonFrameDone(void* data, wl_callback* cb, uint timeMs) nothrow @nogc
{
wsi_callback_destroy(cb);
g_frameCb = null;
g_frames++;
if (g_frames == 1)
instrFirstPixelPresented();
if (g_autoExit && (g_frames >= autoExitFrames || instrNowUs() > autoExitUsCap))
{
g_running = false;
return;
}
render();
}
__gshared wl_registry_listener _error_ app.g_registryListenerg_registryListener = {&onGlobal, &onGlobalRemove};
__gshared xdg_wm_base_listener _error_ app.g_wmBaseListenerg_wmBaseListener = {&onWmBasePing};
__gshared wl_seat_listener _error_ app.g_seatListenerg_seatListener = {&onSeatCapabilities, &onSeatName};
__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};
__gshared wl_callback_listener _error_ app.g_frameListenerg_frameListener = {&onFrameDone};
__gshared wl_keyboard_listener _error_ app.g_keyboardListenerg_keyboardListener = {
&onKbKeymap, &onKbEnter, &onKbLeave, &onKbKey, &onKbModifiers, &onKbRepeatInfo
};
__gshared zwp_text_input_v3_listener _error_ app.g_tiListenerg_tiListener = {
&onTiEnter, &onTiLeave, &onTiPreedit, &onTiCommitString,
&onTiDeleteSurrounding, &onTiDone
};
// --------------------------------------------------------------------- main
int int D main()main()
{
instrInit("f07_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';
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);
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;
}
// The two F07-specific capability probes — each absence is itself a finding.
instrEvent("text_input_manager", "present=%d", g_tiManager !is null ? 1 : 0);
instrEvent("seat", "present=%d", g_seat !is null ? 1 : 0);
wsi_wm_base_add_listener(g_wmBase, &g_wmBaseListener, null);
if (g_seat !is null)
wsi_seat_add_listener(g_seat, &g_seatListener, null);
if (g_tiManager !is null && g_seat !is null)
{
g_ti = wsi_text_input_manager_get_text_input(g_tiManager, g_seat);
wsi_text_input_add_listener(g_ti, &g_tiListener, null);
instrStep("zwp_text_input_manager_v3_get_text_input");
}
g_xkbCtx = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
g_surface = wsi_compositor_create_surface(g_compositor);
g_xdgSurface = wsi_wm_base_get_xdg_surface(g_wmBase, g_surface);
wsi_xdg_surface_add_listener(g_xdgSurface, &g_xdgSurfaceListener, null);
g_toplevel = wsi_xdg_surface_get_toplevel(g_xdgSurface);
wsi_toplevel_add_listener(g_toplevel, &g_toplevelListener, null);
wsi_toplevel_set_title(g_toplevel, "wsi-f07-text-input");
wsi_toplevel_set_app_id(g_toplevel, "wsi-f07-text-input");
instrWindowCreated();
wsi_surface_commit(g_surface); // mandatory no-buffer first commit
// Poll-based pump (the F04 lesson): a blocking wl_display_dispatch would
// hang forever if the compositor goes quiet (sway throttles invisible
// surfaces; an IME run waits on external events), so poll with a timeout
// keeps the wall-clock auto-exit live during protocol silence.
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 (local variable) _error_ pfdpfd = {dispFd, POLLIN, 0};
if (poll(&pfd, 1, 100) > 0)
wl_display_read_events(g_display);
else
wl_display_cancel_read(g_display);
if (wl_display_dispatch_pending(g_display) == -1)
break;
if ((__gshared global) bool app.g_autoExitg_autoExit && instrNowUs() > autoExitUsCap)
(__gshared global) bool app.g_runningg_running = false;
}
// Teardown: children before parents.
if (g_ti !is null)
wsi_text_input_destroy(g_ti);
if (g_tiManager !is null)
wsi_text_input_manager_destroy(g_tiManager);
if (g_keyboard !is null)
wsi_keyboard_destroy(g_keyboard);
if (g_xkbState !is null)
xkb_state_unref(g_xkbState);
if (g_xkbKeymap !is null)
xkb_keymap_unref(g_xkbKeymap);
if (g_xkbCtx !is null)
xkb_context_unref(g_xkbCtx);
foreach (ref (parameter) bb; g_buffers)
if (b.b.handlehandle !is null)
{
wsi_buffer_destroy(b.b.handlehandle);
munmap(b.b.pixelspixels, b.b.byteSizebyteSize);
}
if (g_frameCb !is null)
wsi_callback_destroy(g_frameCb);
wsi_toplevel_destroy(g_toplevel);
wsi_xdg_surface_destroy(g_xdgSurface);
wsi_surface_destroy(g_surface);
wsi_wm_base_destroy(g_wmBase);
if (g_seat !is null)
wl_proxy_destroy(cast(wl_proxy*) g_seat);
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: frames=%d ti_present=%d ti_commits=%u final_text=\"%s\"\n",
(__gshared global) int app.g_framesg_frames, g_ti !is null ? 1 : 0, (__gshared global) uint app.g_tiCommitsg_tiCommits, (__gshared global) char[241] app.g_textg_text.(constant) char* char[241].ptr = &g_textptr);
return 0;
}