diff options
| author | Adam Malczewski <[email protected]> | 2026-04-11 19:06:35 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-04-11 19:06:35 +0900 |
| commit | 93f50d20a021150a0b95242be0d5dd5cae9d0185 (patch) | |
| tree | 48054581d580974651260900d1ef79d370872952 /.rules/plan | |
| download | winman-raylib-93f50d20a021150a0b95242be0d5dd5cae9d0185.tar.gz winman-raylib-93f50d20a021150a0b95242be0d5dd5cae9d0185.zip | |
plan and researchmain
Diffstat (limited to '.rules/plan')
| -rw-r--r-- | .rules/plan/README.md | 50 | ||||
| -rw-r--r-- | .rules/plan/phase-00-skeleton.md | 72 | ||||
| -rw-r--r-- | .rules/plan/phase-01-raylib-window.md | 19 | ||||
| -rw-r--r-- | .rules/plan/phase-02-window-manager.md | 93 | ||||
| -rw-r--r-- | .rules/plan/phase-03-compositing.md | 106 | ||||
| -rw-r--r-- | .rules/plan/phase-04-input.md | 50 | ||||
| -rw-r--r-- | .rules/plan/phase-05-lifecycle.md | 45 | ||||
| -rw-r--r-- | .rules/plan/phase-06-effects.md | 44 | ||||
| -rw-r--r-- | .rules/plan/phase-07-ewmh.md | 46 | ||||
| -rw-r--r-- | .rules/plan/phase-08-zero-copy.md | 39 | ||||
| -rw-r--r-- | .rules/plan/phase-09-cursor.md | 30 | ||||
| -rw-r--r-- | .rules/plan/phase-10-robustness.md | 48 |
12 files changed, 642 insertions, 0 deletions
diff --git a/.rules/plan/README.md b/.rules/plan/README.md new file mode 100644 index 0000000..a50dabf --- /dev/null +++ b/.rules/plan/README.md @@ -0,0 +1,50 @@ +# winman-raylib — Implementation Plan + +A compositing X11 window manager powered by raylib, written in C. + +Each step is atomic: it produces something you can build, run, and visually +verify before moving to the next step. Each phase is documented in its own +file below. + +--- + +## Phases + +| Phase | File | Summary | +|-------|------|---------| +| 0 | [phase-00-skeleton.md](plan/phase-00-skeleton.md) | Project skeleton & dev environment | +| 1 | [phase-01-raylib-window.md](plan/phase-01-raylib-window.md) | Raylib window inside Xephyr | +| 2 | [phase-02-window-manager.md](plan/phase-02-window-manager.md) | Become a window manager | +| 3 | [phase-03-compositing.md](plan/phase-03-compositing.md) | Redirect & capture window contents | +| 4 | [phase-04-input.md](plan/phase-04-input.md) | Input & focus | +| 5 | [phase-05-lifecycle.md](plan/phase-05-lifecycle.md) | Window lifecycle polish | +| 6 | [phase-06-effects.md](plan/phase-06-effects.md) | Visual effects | +| 7 | [phase-07-ewmh.md](plan/phase-07-ewmh.md) | EWMH basics | +| 8 | [phase-08-zero-copy.md](plan/phase-08-zero-copy.md) | GLX texture-from-pixmap (zero-copy) | +| 9 | [phase-09-cursor.md](plan/phase-09-cursor.md) | Cursor rendering | +| 10 | [phase-10-robustness.md](plan/phase-10-robustness.md) | Robustness & cleanup | + +--- + +## Future Phases (not yet planned in detail) + +- **Phase 11** — Multi-monitor support (XRandR) +- **Phase 12** — Virtual desktops / workspaces +- **Phase 13** — Window animations (open, close, minimize) +- **Phase 14** — Blur and advanced shader effects +- **Phase 15** — RmlUi integration for HTML/CSS-driven decorations/panels +- **Phase 16** — Configuration system (keybinds, rules, themes) +- **Phase 17** — Composite Overlay Window (COW) for production use outside Xephyr +- **Phase 18** — Tiling layout modes + +--- + +## Architecture Decisions + +- **Language:** C +- **Build system:** Make +- **Rendering:** raylib (creates the GL context and window) +- **X11 plumbing:** raw Xlib + GLX alongside raylib in the same process +- **Dev environment:** Xephyr nested X server on `:1` +- **Compositing surface:** raylib's own window (not the COW), suitable for Xephyr dev; COW migration in Phase 17 +- **Initial texture path:** CPU copy via `XGetImage` (Phase 3), upgraded to zero-copy `GLX_EXT_texture_from_pixmap` in Phase 8 diff --git a/.rules/plan/phase-00-skeleton.md b/.rules/plan/phase-00-skeleton.md new file mode 100644 index 0000000..f6bac05 --- /dev/null +++ b/.rules/plan/phase-00-skeleton.md @@ -0,0 +1,72 @@ +# Phase 0 — Project Skeleton & Dev Environment + +--- + +## Step 0.1 — Makefile and empty main + +Create the basic project structure: + +``` +src/ + main.c — int main() { return 0; } +bin/ + build.sh — runs make + run.sh — builds then launches the binary +Makefile — compiles src/*.c → build/winman-raylib +``` + +**Makefile** compiles with `cc`, links nothing yet. Uses `pkg-config` for +flags. Output goes to `build/`. + +**Verify:** `./bin/build.sh` succeeds, `./build/winman-raylib` runs and +exits silently with code 0. + +--- + +## Step 0.2 — Install dependencies and verify Xephyr + +Create `bin/setup-deps.sh` that installs all needed packages (Arch +`pacman` commands): + +- **Build:** `base-devel`, `raylib` +- **X11 libs:** `libx11`, `libxcomposite`, `libxdamage`, `libxfixes`, + `libxrender`, `libxext`, `libxrandr` +- **GL:** `mesa`, `glew` +- **Dev tools:** `xorg-server-xephyr`, `xorg-xinit`, `xorg-xauth` +- **Test clients:** `xorg-xeyes`, `xorg-xclock`, `xterm`, + `xorg-xwininfo`, `xorg-xprop`, `xorg-xdpyinfo` + +**Verify:** `./bin/setup-deps.sh` runs without errors. `Xephyr -help` +prints usage. `pkg-config --libs raylib x11 xcomposite xdamage xfixes` +prints flags. + +--- + +## Step 0.3 — Xephyr launch/kill scripts + +Create two scripts: + +- `bin/xephyr-start.sh` — Launches Xephyr on `:1` with + `-br -ac -noreset -resizeable -no-host-grab -screen 1280x800`. + Prints the PID. Exits immediately (Xephyr runs in background). +- `bin/xephyr-stop.sh` — Kills the Xephyr process cleanly. + +**Verify:** `./bin/xephyr-start.sh` opens a black Xephyr window. +`DISPLAY=:1 xeyes &` shows xeyes inside it. `./bin/xephyr-stop.sh` +closes everything. + +--- + +## Step 0.4 — `bin/run.sh` — full dev loop script + +A single script that: + +1. Builds the project (`make`). +2. Starts Xephyr on `:1` if not already running. +3. Launches `build/winman-raylib` with `DISPLAY=:1`. + +Accepts an optional `--clients` flag that also spawns `xeyes`, `xclock`, +and `xterm` into `:1` after a short delay. + +**Verify:** `./bin/run.sh --clients` opens Xephyr, launches the (still +empty) binary, and spawns the test clients into the Xephyr window. diff --git a/.rules/plan/phase-01-raylib-window.md b/.rules/plan/phase-01-raylib-window.md new file mode 100644 index 0000000..d40959b --- /dev/null +++ b/.rules/plan/phase-01-raylib-window.md @@ -0,0 +1,19 @@ +# Phase 1 — Raylib Window Inside Xephyr + +--- + +## Step 1.1 — Open a raylib window + +Update `main.c` to: + +1. Call `InitWindow(1280, 800, "winman-raylib")`. +2. Enter a loop: `BeginDrawing()`, clear to dark blue, `DrawText("winman-raylib", ...)`, + `EndDrawing()`. +3. `CloseWindow()` on exit. + +Update the Makefile to link raylib (`pkg-config --libs raylib`), plus +`-lGL -lm -lpthread -ldl`. + +**Verify:** `./bin/run.sh` opens Xephyr and a raylib window appears inside +it showing "winman-raylib" text on a dark blue background. The window +responds to the close button / Escape key. diff --git a/.rules/plan/phase-02-window-manager.md b/.rules/plan/phase-02-window-manager.md new file mode 100644 index 0000000..2627a57 --- /dev/null +++ b/.rules/plan/phase-02-window-manager.md @@ -0,0 +1,93 @@ +# Phase 2 — Become a Window Manager + +--- + +## Step 2.1 — Open an Xlib display connection alongside raylib + +Before `InitWindow()`, call `XOpenDisplay(NULL)` to get a `Display*`. +Store it globally. Get the root window with `DefaultRootWindow()`. After +the raylib loop exits, `XCloseDisplay()`. + +Print the display name and root window ID to confirm. + +**Verify:** The output prints something like `Display: :1, Root: 0x...`. +Raylib window still works as before. + +--- + +## Step 2.2 — Register as the window manager (SubstructureRedirect) + +After opening the display, call: + +```c +XSelectInput(dpy, root, + SubstructureRedirectMask | + SubstructureNotifyMask | + StructureNotifyMask); +XSync(dpy, False); +``` + +Set a custom X error handler that catches `BadAccess` (another WM is +already running) and prints a clear message. + +**Verify:** Running in Xephyr succeeds (no other WM). Running a second +instance prints "Another window manager is already running" and exits. + +--- + +## Step 2.3 — Process X events in the raylib loop + +Inside the raylib `while (!WindowShouldClose())` loop, add a non-blocking +X event drain: + +```c +while (XPending(dpy) > 0) { + XEvent ev; + XNextEvent(dpy, &ev); + printf("Event: %d\n", ev.type); +} +``` + +**Verify:** `./bin/run.sh --clients` — launching `xeyes` or `xterm` into +`:1` prints `MapRequest`, `ConfigureRequest`, etc. to the terminal. The +clients don't appear yet (because we don't handle `MapRequest`). + +--- + +## Step 2.4 — Handle MapRequest and ConfigureRequest (basic) + +Add a simple event handler: + +- **`MapRequest`** → call `XMapWindow(dpy, ev.xmaprequest.window)`. +- **`ConfigureRequest`** → honor the request by calling + `XConfigureWindow()` with the requested values. + +**Verify:** `./bin/run.sh --clients` — `xeyes`, `xclock`, and `xterm` now +appear inside the Xephyr window. They're drawn by X directly (not +composited yet), floating on top of/beside the raylib window. You can type +in `xterm`. + +--- + +## Step 2.5 — Track managed windows in a list + +Create a simple dynamic array (or linked list) of `WmWindow` structs: + +```c +typedef struct { + Window xwin; + int x, y; + unsigned int width, height; + bool mapped; +} WmWindow; +``` + +- On `MapRequest`: add the window to the list, then `XMapWindow`. +- On `UnmapNotify`: mark `mapped = false`. +- On `DestroyNotify`: remove from the list. +- On `ConfigureRequest`: update stored geometry, then `XConfigureWindow`. + +Print the window list count on each change. + +**Verify:** Spawn/close clients in `:1`. The log shows windows being added +and removed. Count matches what's visible. diff --git a/.rules/plan/phase-03-compositing.md b/.rules/plan/phase-03-compositing.md new file mode 100644 index 0000000..a85d12b --- /dev/null +++ b/.rules/plan/phase-03-compositing.md @@ -0,0 +1,106 @@ +# Phase 3 — Compositing: Redirect & Capture Window Contents + +--- + +## Step 3.1 — Query and enable the Composite extension + +At startup (after becoming WM), call: + +```c +XCompositeQueryExtension(dpy, &composite_event, &composite_error); +XCompositeQueryVersion(dpy, &major, &minor); +XCompositeRedirectSubwindows(dpy, root, CompositeRedirectManual); +``` + +Print the extension version to confirm. + +**Verify:** Prints "Composite extension: 0.4" (or similar). Clients +launched into `:1` are now **invisible** (redirected to off-screen pixmaps +but nobody is painting them). This is expected — the WM is responsible for +compositing now. + +--- + +## Step 3.2 — Name window pixmaps + +When a window is mapped (in the `MapRequest` handler or after +`XMapWindow`), call: + +```c +Pixmap pixmap = XCompositeNameWindowPixmap(dpy, win->xwin); +``` + +Store the `Pixmap` in the `WmWindow` struct. On `ConfigureNotify` (resize), +free the old pixmap and re-obtain it. + +**Verify:** No visual change yet (windows are still invisible), but no X +errors are printed. Confirm pixmap handles are non-zero via log output. + +--- + +## Step 3.3 — Read pixmap into a raylib Texture (CPU copy path) + +For each mapped window with a valid pixmap: + +1. Call `XGetImage(dpy, pixmap, 0, 0, width, height, AllPlanes, ZPixmap)` + to read the pixel data. +2. Convert the BGRA data (X11 format) to RGBA. +3. Create a raylib `Image` from the buffer, then `LoadTextureFromImage()`. +4. Store the `Texture2D` in the `WmWindow` struct. + +In the raylib draw loop, draw each managed window's texture at its +position using `DrawTexture()`. + +**Verify:** `./bin/run.sh --clients` — `xeyes`, `xclock`, and `xterm` +appear as textured quads rendered by raylib on the dark blue background. +They are static snapshots (no live updates yet). The raylib window is the +compositor. + +--- + +## Step 3.4 — Enable the Damage extension for live updates + +At startup, query and enable Damage: + +```c +XDamageQueryExtension(dpy, &damage_event, &damage_error); +``` + +For each managed window, create a damage object: + +```c +win->damage = XDamageCreate(dpy, win->xwin, XDamageReportNonEmpty); +``` + +In the event loop, handle `DamageNotify`: + +```c +if (ev.type == damage_event + XDamageNotify) { + // mark the window dirty + win->dirty = true; +} +``` + +When a window is dirty, before drawing, re-read the pixmap via +`XGetImage`, update the texture with `UpdateTexture()`, then call +`XDamageSubtract(dpy, win->damage, None, None)`. + +**Verify:** `xeyes` follows your mouse cursor in real-time. `xclock` +ticks. Typing in `xterm` is visible immediately. This is a working (CPU- +copied) compositor. + +--- + +## Step 3.5 — Handle window stacking order + +Draw windows in the correct stacking order (bottom to top). Maintain the +list order to match X's stacking order. Use `XQueryTree()` at startup to +get the initial order, and update on `ConfigureNotify` with +`Above`/`Below` sibling fields, or re-query when the stack changes. + +Also handle `XRaiseWindow` / `XLowerWindow` requests via +`ConfigureRequest`. + +**Verify:** Overlapping windows draw in the correct order. Clicking +`xterm` (if focus-follows-click is wired) or manually calling +`DISPLAY=:1 xdotool windowraise <id>` changes the draw order. diff --git a/.rules/plan/phase-04-input.md b/.rules/plan/phase-04-input.md new file mode 100644 index 0000000..439d215 --- /dev/null +++ b/.rules/plan/phase-04-input.md @@ -0,0 +1,50 @@ +# Phase 4 — Input & Focus + +--- + +## Step 4.1 — Click-to-focus and raise + +On `ButtonPress` events on managed windows, set focus with +`XSetInputFocus(dpy, win->xwin, RevertToParent, CurrentTime)` and raise +the window with `XRaiseWindow()`. + +Register for `ButtonPressMask` on all managed windows using a passive +grab: + +```c +XGrabButton(dpy, AnyButton, AnyModifier, win->xwin, True, + ButtonPressMask, GrabModeSync, GrabModeAsync, None, None); +``` + +On `ButtonPress`, set focus, raise, then `XAllowEvents(dpy, ReplayPointer, +CurrentTime)` to pass the click through to the client. + +**Verify:** Click on `xterm` — it receives focus and comes to front. +Type in it. Click `xeyes` — it comes to front. Focus switching works. + +--- + +## Step 4.2 — Window dragging with Mod+Click + +Grab `Mod4 + Button1` (Super+Click) on the root window. On press, start +tracking a drag. On motion, `XMoveWindow()` the client and update the +stored position. On release, stop. + +```c +XGrabButton(dpy, Button1, Mod4Mask, root, True, + ButtonPressMask | ButtonReleaseMask | PointerMotionMask, + GrabModeAsync, GrabModeAsync, None, None); +``` + +**Verify:** Super+Click and drag moves windows around inside Xephyr. +Release drops them. The composited view updates in real-time. + +--- + +## Step 4.3 — Window resizing with Mod+RightClick + +Same pattern as dragging but with `Mod4 + Button3`. On motion, call +`XResizeWindow()`. Re-obtain the pixmap and texture after resize. + +**Verify:** Super+RightClick and drag resizes windows. The texture +updates to show the new content. diff --git a/.rules/plan/phase-05-lifecycle.md b/.rules/plan/phase-05-lifecycle.md new file mode 100644 index 0000000..89f6a45 --- /dev/null +++ b/.rules/plan/phase-05-lifecycle.md @@ -0,0 +1,45 @@ +# Phase 5 — Window Lifecycle Polish + +--- + +## Step 5.1 — Graceful close (WM_DELETE_WINDOW) + +When the user presses a keybind (e.g., `Mod4+Q`), check if the focused +window supports `WM_DELETE_WINDOW` in its `WM_PROTOCOLS` property. If yes, +send a `ClientMessage`. If no, call `XKillClient()`. + +Register the keybind with `XGrabKey()`. + +**Verify:** `Mod4+Q` closes `xterm` gracefully (it gets a chance to +confirm). Force-kills uncooperative clients. + +--- + +## Step 5.2 — Handle UnmapNotify and DestroyNotify properly + +On `UnmapNotify`: +- Free the pixmap and texture. +- Mark window as unmapped. +- Destroy the damage object. + +On `DestroyNotify`: +- Remove the window from the managed list entirely. +- Clean up all resources. + +Handle re-mapping (a window that was unmapped then mapped again) by +re-creating everything. + +**Verify:** Close and reopen clients repeatedly. No memory leaks (watch +with `valgrind` or check texture count). No X errors. No stale windows +in the compositor view. + +--- + +## Step 5.3 — Manage pre-existing windows on startup + +After becoming the WM, call `XQueryTree()` to get all existing children +of root. For each that has `map_state == IsViewable`, treat it as if we +received a `MapRequest`: add to the list, set up pixmap, damage, texture. + +**Verify:** Start some clients in `:1` *before* the WM. Then start the +WM. Pre-existing windows appear immediately in the composited view. diff --git a/.rules/plan/phase-06-effects.md b/.rules/plan/phase-06-effects.md new file mode 100644 index 0000000..6a21f72 --- /dev/null +++ b/.rules/plan/phase-06-effects.md @@ -0,0 +1,44 @@ +# Phase 6 — Visual Effects (Compositor Features) + +--- + +## Step 6.1 — Window decorations (title bars) + +For each managed window, draw a simple title bar above it: +- A colored rectangle. +- The window title (from `WM_NAME` or `_NET_WM_NAME` property). +- A close button (clickable, sends `WM_DELETE_WINDOW`). + +Use raylib's `DrawRectangle()` and `DrawText()`. + +**Verify:** Each window has a title bar showing its name. Clicking the +close button closes the window. + +--- + +## Step 6.2 — Drop shadows + +Draw a soft shadow behind each window. Approach: +- Precompute a shadow texture (gaussian blur of a solid rect) at startup. +- Draw it as a 9-slice scaled quad behind each window, offset down and + right by a few pixels. + +Or use raylib's shader system with a blur shader on an expanded quad. + +**Verify:** Each window has a soft drop shadow. Overlapping windows show +correct shadow layering. + +--- + +## Step 6.3 — Window transparency / opacity + +Read `_NET_WM_WINDOW_OPACITY` from each window's properties. Apply it +when drawing the texture (raylib's `DrawTexturePro` with tint alpha, or +a custom shader uniform). + +**Verify:** Run: +```bash +DISPLAY=:1 xprop -f _NET_WM_WINDOW_OPACITY 32c \ + -set _NET_WM_WINDOW_OPACITY 0x7FFFFFFF -id <wid> +``` +to set 50% opacity on a window. It becomes translucent in the compositor. diff --git a/.rules/plan/phase-07-ewmh.md b/.rules/plan/phase-07-ewmh.md new file mode 100644 index 0000000..9c300e2 --- /dev/null +++ b/.rules/plan/phase-07-ewmh.md @@ -0,0 +1,46 @@ +# Phase 7 — EWMH Basics + +--- + +## Step 7.1 — Set root window EWMH properties + +At startup, set on the root window: +- `_NET_SUPPORTED` — list of atoms we support. +- `_NET_SUPPORTING_WM_CHECK` — create a child window, set `_NET_WM_NAME` + on it to `"winman-raylib"`. +- `_NET_CLIENT_LIST` — list of managed windows. +- `_NET_ACTIVE_WINDOW` — currently focused window. +- `_NET_NUMBER_OF_DESKTOPS` — `1` (for now). +- `_NET_CURRENT_DESKTOP` — `0`. + +Update `_NET_CLIENT_LIST` and `_NET_ACTIVE_WINDOW` as windows are +added/removed/focused. + +**Verify:** `DISPLAY=:1 wmctrl -m` shows the WM name. `wmctrl -l` lists +managed windows. `xprop -root _NET_SUPPORTED` shows the atom list. + +--- + +## Step 7.2 — Handle _NET_WM_WINDOW_TYPE + +Read `_NET_WM_WINDOW_TYPE` from each window on map. Handle at minimum: +- `_NET_WM_WINDOW_TYPE_NORMAL` — standard management. +- `_NET_WM_WINDOW_TYPE_DIALOG` — don't decorate differently (for now), + but keep above parent if `WM_TRANSIENT_FOR` is set. +- `_NET_WM_WINDOW_TYPE_DOCK` — don't manage (panels); reserve screen + space per `_NET_WM_STRUT` / `_NET_WM_STRUT_PARTIAL`. + +**Verify:** A dock-type window (e.g., `DISPLAY=:1 tint2` or a custom +test client) is not given a title bar and stays above other windows. + +--- + +## Step 7.3 — Handle _NET_WM_STATE (fullscreen) + +Listen for `_NET_WM_STATE` `ClientMessage` requests. Implement at +minimum: +- `_NET_WM_STATE_FULLSCREEN` — resize window to fill screen, remove + decorations, draw above all others. + +**Verify:** `DISPLAY=:1 wmctrl -r :ACTIVE: -b toggle,fullscreen` toggles +the focused window to fullscreen and back. diff --git a/.rules/plan/phase-08-zero-copy.md b/.rules/plan/phase-08-zero-copy.md new file mode 100644 index 0000000..56f99c3 --- /dev/null +++ b/.rules/plan/phase-08-zero-copy.md @@ -0,0 +1,39 @@ +# Phase 8 — GLX Texture-from-Pixmap (Zero-Copy Upgrade) + +--- + +## Step 8.1 — Replace XGetImage with GLX_EXT_texture_from_pixmap + +This is the performance upgrade. Instead of reading pixels through the +CPU, bind the X pixmap directly as an OpenGL texture: + +1. Find an FBConfig that supports `GLX_BIND_TO_TEXTURE_RGBA_EXT`. +2. Create a `GLXPixmap` from the X `Pixmap`. +3. Call `glXBindTexImageEXT()` to bind it as a GL texture. +4. Wrap the GL texture ID in a raylib `Texture2D` struct (set `.id` + directly, since raylib textures are just GL texture handles with + width/height/format metadata). + +On damage: `glXReleaseTexImageEXT` then `glXBindTexImageEXT` to refresh. +On resize: destroy and recreate the GLX pixmap. + +Handle `GLX_Y_INVERTED_EXT` — if the pixmap is Y-inverted, flip the +texture coordinates when drawing (use `DrawTexturePro` with a negative +source height, or flip in a shader). + +**Verify:** Same visual result as Step 3.4 but with dramatically less +CPU usage. Confirm with `top` or `perf` that `XGetImage` is no longer +called. `xeyes` still tracks the mouse smoothly. Resizing windows +works without artifacts. + +**Note:** This step may not work inside Xephyr (which uses software +rendering). If `GLX_EXT_texture_from_pixmap` is not available in Xephyr, +keep the CPU-copy path as a fallback and test zero-copy on a real X +session or VM with GPU passthrough. Implement runtime detection: + +```c +const char *glx_exts = glXQueryExtensionsString(dpy, screen); +bool has_tfp = strstr(glx_exts, "GLX_EXT_texture_from_pixmap") != NULL; +``` + +Use `has_tfp` to pick the code path. diff --git a/.rules/plan/phase-09-cursor.md b/.rules/plan/phase-09-cursor.md new file mode 100644 index 0000000..11d301f --- /dev/null +++ b/.rules/plan/phase-09-cursor.md @@ -0,0 +1,30 @@ +# Phase 9 — Cursor Rendering + +--- + +## Step 9.1 — Render the cursor in the compositor + +Since compositing may obscure the hardware cursor in certain +configurations, render it ourselves: + +1. Call `XFixesGetCursorImage(dpy)` to get the current cursor bitmap + (ARGB pixel data, width, height, hotspot). +2. Convert to a raylib `Texture2D`. +3. Draw it at the pointer position each frame (query with + `XQueryPointer()`). +4. Optionally call `XFixesHideCursor()` on the root and draw our own + cursor exclusively. + +Subscribe to cursor change notifications: + +```c +XFixesSelectCursorInput(dpy, root, XFixesDisplayCursorNotifyMask); +``` + +Cache the cursor texture and only update it when an +`XFixesCursorNotify` event fires (cursor shape changed — e.g., from +arrow to text beam when hovering over `xterm`). + +**Verify:** The cursor is visible and correct when moving over composited +windows. Cursor changes (e.g., text cursor in xterm, resize arrows at +window edges) are reflected. No flickering or offset. diff --git a/.rules/plan/phase-10-robustness.md b/.rules/plan/phase-10-robustness.md new file mode 100644 index 0000000..39c719d --- /dev/null +++ b/.rules/plan/phase-10-robustness.md @@ -0,0 +1,48 @@ +# Phase 10 — Robustness & Cleanup + +--- + +## Step 10.1 — Proper error handling + +- Set `XSetErrorHandler()` to a handler that logs but doesn't crash on + `BadWindow`, `BadPixmap`, `BadDrawable` (windows can vanish between + operations). +- Wrap all X calls that reference a window in the handler's + error-checked scope. +- On fatal errors (`XSetIOErrorHandler`), clean up gracefully. + +**Verify:** Rapidly spawn and kill clients (a stress loop script). No +crashes, no X error floods. Clean log output. + +--- + +## Step 10.2 — Clean shutdown + +On `SIGINT`/`SIGTERM` or raylib `WindowShouldClose()`: + +1. Unredirect all windows (`XCompositeUnredirectSubwindows`). +2. Free all pixmaps, textures, damage objects. +3. Release the overlay (if used later). +4. `XCloseDisplay()`. +5. `CloseWindow()` (raylib). + +**Verify:** Killing the WM with Ctrl+C cleanly restores windows in +Xephyr (they become visible via X's normal rendering again). No +orphaned resources. + +--- + +## Step 10.3 — Stress test script + +Create `bin/stress-test.sh` that: + +1. Starts Xephyr. +2. Starts the WM. +3. Rapidly spawns 20 `xterm` instances. +4. Randomly moves, resizes, and closes them via `xdotool`. +5. Checks that `wmctrl -l` count matches expectations. +6. Screenshots the result. +7. Tears everything down. + +**Verify:** No crashes, no hangs, no leaked windows. Screenshot looks +sane. |
