From a519435bfa263c3de8b8c13433adbe0586b3a181 Mon Sep 17 00:00:00 2001 From: Window Maker Date: Sat, 23 May 2026 11:10:25 +0200 Subject: [PATCH] x11 doc --- X11_IMPLEMENTATION.md | 989 ++++++++++++++++++++++++++++++++++++++++++ src/appicon.c | 2 +- src/event.c | 4 +- 3 files changed, 992 insertions(+), 3 deletions(-) create mode 100644 X11_IMPLEMENTATION.md diff --git a/X11_IMPLEMENTATION.md b/X11_IMPLEMENTATION.md new file mode 100644 index 0000000..2f76aae --- /dev/null +++ b/X11_IMPLEMENTATION.md @@ -0,0 +1,989 @@ +# WindowMaker X11 Implementation — Baseline Reference + +Cross-referenced against the feature breakdown in `../wmaker/WAYLAND_ROADMAP.md`. + +This document describes how the **original upstream WindowMaker** (this codebase, +`wmaker-git`) provides core window manager functionality using X11/Xlib directly. +It serves as the authoritative baseline for verifying correctness of the Wayland port. + +> **Note:** This codebase has NO backend abstraction layer. All X11 calls are made +> directly inline from the policy/logic code. The vtable-based `WMBackend` abstraction +> exists only in the ported version at `../wmaker/`. + +--- + +## Table of Contents + +1. [Startup & Initialization](#1-startup--initialization) +2. [Event Loop & Dispatch](#2-event-loop--dispatch) +3. [Phase A: Core Window Management](#3-phase-a-core-window-management) + - A1: Stacking + - A2: Maximize + - A3: Shade + - A4: Workspace Switching + - A5: Alt-Tab Window Cycling + - A6: Iconification + - A7: Window Placement +4. [Phase B: Dock & App Icons](#4-phase-b-dock--app-icons) +5. [Phase C: Visual Polish & Animations](#5-phase-c-visual-polish--animations) +6. [Phase G: Session & Persistence](#6-phase-g-session--persistence) +7. [Phase I: EWMH Compliance](#7-phase-i-ewmh-compliance) +8. [X11 Mechanisms Summary](#8-x11-mechanisms-summary) + +--- + +## 1. Startup & Initialization + +**Files:** `src/main.c`, `src/startup.c`, `src/screen.c` + +### Entry Point (`main.c`) + +``` +main() → fork monitor process (crash recovery supervisor) + → real_main() [with --for-real flag] + → parse CLI args (--no-dock, --no-clip, --replace, --visual-id, etc.) + → setlocale / i18n init + → XOpenDisplay(display_name) + → fcntl(ConnectionNumber(dpy), F_SETFD, FD_CLOEXEC) + → StartUp() + → execInitScript("autostart") + → setup inotify on ~/GNUstep/Defaults/ + → EventLoop() [never returns] +``` + +The monitor process (`MonitorLoop`) watches the child via `waitpid()` and re-execs +wmaker on abnormal exit (crash recovery). + +### Initialization Sequence (`startup.c`) + +`StartUp(Bool defaultScreenOnly)` orchestrates: + +1. **Modifier detection** — `getOffendingModifiers()` uses `XGetModifierMapping()` to + find NumLock/ScrollLock keycodes, enabling grabs that work regardless of lock state. + +2. **Atom interning** — `XInternAtoms()` batch-creates ~60 atoms: + `WM_STATE`, `WM_PROTOCOLS`, `WM_DELETE_WINDOW`, `WM_TAKE_FOCUS`, + `_WINDOWMAKER_*`, `_NET_WM_*`, `_GNUSTEP_WM_ATTR`, etc. + +3. **Cursor creation** — `XCreateFontCursor()` for all WM cursors (arrow, move, + resize corners, wait, question, etc.). + +4. **Signal handlers** — SIGTERM→exit, SIGUSR1→restart, SIGUSR2→reread defaults, + SIGCHLD→`buryChild()` (waitpid reaper). + +5. **Event hook** — `WMHookEventHandler(DispatchEvent)` connects the WINGs event + pump to the WM's dispatch function. + +6. **Defaults** — Reads WindowMaker, WMRootMenu, WMWindowAttributes domains. + +7. **Extensions** — Queries XShape, XRandR, XKB availability and event bases. + +8. **Per-screen init** — For each X screen: `wScreenInit()` → `wScreenRestoreState()` + → `manageAllWindows()`. + +### Screen Initialization (`screen.c`) + +`wScreenInit(int screen_number)`: + +1. **Claim WM role** — `XSelectInput(root_win, SubstructureRedirectMask | ...)`. + If this fails (BadAccess), another WM is running. This is THE mechanism that + makes a process the window manager on X11. + +2. **WM replacement** — `replace_existing_wm()` implements ICCCM 2.0 WM_Sn selection + protocol: acquire selection ownership, wait for old WM to release, announce via + ClientMessage. + +3. **Raster context** — `RCreateContext()` creates the wrlib rendering context + (Visual, Colormap, GC for image operations). + +4. **WINGs screen** — `WMCreateScreenWithRContext()` initializes the widget toolkit. + +5. **Graphics contexts** — `allocGCs()` creates 7 GCs: stipple, icon_select, + frame (XOR mode for wireframes), line, copy, draw, mono. + +6. **Internal windows** — `createInternalWindows()`: + - `no_focus_win` — InputOnly window as focus fallback (prevents focus going to root) + - `dock_shadow` — For dock icon drag feedback + - `workspace_name` — Overlay for workspace switch display + +7. **EWMH** — `wNETWMInitStuff()` sets `_NET_SUPPORTED`, `_NET_WM_NAME`, etc. + +8. **Existing windows** — `manageAllWindows()` calls `XQueryTree(root)` then + `wManageWindow()` on each child (adopting pre-existing clients at startup). + + +--- + +## 2. Event Loop & Dispatch + +**File:** `src/event.c` + +### The Loop + +```c +void EventLoop(void) +{ + while (1) { + WMNextEvent(dpy, &event); // blocks on XNextEvent() + WMHandleEvent(&event); // calls DispatchEvent() + } +} +``` + +Also polls inotify fd for config file changes between events. + +### Dispatch Table + +`DispatchEvent(XEvent *event)` first checks global state flags: +- `WSTATE_NEED_EXIT` → `Shutdown(WME_NORMAL)` +- `WSTATE_NEED_RESTART` → `Restart(NULL, True)` +- `WSTATE_NEED_REREAD` → `wDefaultsCheckDomains()` + +Then dispatches by `event->type`: + +| X11 Event | Handler | Action | +|-----------|---------|--------| +| `MapRequest` | `handleMapRequest()` | New window → `wManageWindow()`; existing → deiconify/unshade | +| `ConfigureRequest` | `handleConfigureRequest()` | → `wClientConfigure()` | +| `DestroyNotify` | `handleDestroyNotify()` | → `wUnmanageWindow(wwin, False, True)` | +| `UnmapNotify` | `handleUnmapNotify()` | → `wUnmanageWindow(wwin, True, False)` | +| `PropertyNotify` | `handlePropertyNotify()` | → `wClientCheckProperty()` (react to hint changes) | +| `ClientMessage` | `handleClientMessage()` | WM_CHANGE_STATE, EWMH, _WINDOWMAKER_COMMAND | +| `KeyPress` | `handleKeyPress()` | Keyboard shortcut dispatch | +| `ButtonPress` | `handleButtonPress()` | Mouse clicks on frames, root, icons | +| `EnterNotify` | `handleEnterNotify()` | Focus-follows-mouse | +| `LeaveNotify` | `handleLeaveNotify()` | Colormap/focus tracking | +| `FocusIn` | `handleFocusIn()` | Focus state tracking | +| `Expose` | `handleExpose()` | Repaint via `WObjDescriptor` | +| `MappingNotify` | — | `XRefreshKeyboardMapping()` | +| Extensions | `handleExtensions()` | XShape, XRandR, XKB events | + +### Window Lookup + +Events are routed to the correct `WWindow` via: +```c +wwin = wWindowFor(event->xany.window); // uses XFindContext() +``` + +The context table (`XSaveContext`/`XFindContext`) maps X Window IDs → `WWindow*` +pointers. Separate contexts exist for client windows, app windows, and stack entries. + +### Child Process Tracking + +`handleDeadProcess()` is called at the top of `DispatchEvent` to process any +children that died since the last event (collected by SIGCHLD → `buryChild()` +via `waitpid(WNOHANG)`). Death handlers registered via `wAddDeathHandler()` are +invoked here (used by dock app launch tracking). + + +--- + +## 3. Phase A: Core Window Management + +### Window Lifecycle (Manage / Unmanage) + +**File:** `src/window.c` + +#### `wManageWindow(WScreen *scr, Window window)` — Adopt a Client + +This is the central ~500-line function that turns a raw X11 client window into a +managed WWindow. Sequence: + +1. **`XGrabServer()`** — Atomic operation (prevents races with client). +2. **`XGetWindowAttributes()`** — Validate window exists, get geometry, check + `override_redirect` (skip if true — popups/tooltips manage themselves). +3. **Fetch all hints:** + - `XGetWMHints()` — urgency, icon pixmap, group leader, input model + - `PropGetWMClass()` — instance + class name (for per-app settings) + - `PropGetGNUstepWMAttr()` — GNUstep-specific attributes + - `PropGetProtocols()` — WM_DELETE_WINDOW, WM_TAKE_FOCUS support + - `XGetTransientForHint()` — transient-for relationship + - `wClientGetNormalHints()` — WM_NORMAL_HINTS (min/max/inc size, gravity) + - EWMH properties — `_NET_WM_WINDOW_TYPE`, `_NET_WM_STATE`, etc. +4. **`XSaveContext()`** — Register client window → WWindow mapping. +5. **`XChangeWindowAttributes()`** — Set event mask on client (StructureNotify, + PropertyChange, Colormap, FocusChange). +6. **`wWindowSetupInitialAttributes()`** — Multi-layered attribute resolution: + global defaults → GNUstep hints → MWM hints → EWMH → per-app user overrides. + Determines: decoration flags, window level, skip-window-list, omnipresent, etc. +7. **`wFrameWindowCreate()`** — Create the decoration frame (see framewin.c below). +8. **`XReparentWindow(client, frame, x, y)`** — THE fundamental WM operation. + Client becomes a child of the frame window. +9. **`wWindowConfigure()`** — Apply computed geometry (respecting size hints, gravity). +10. **Application tracking** — `wApplicationCreate()` groups windows by leader. +11. **`XAddToSaveSet()`** — Ensures client survives WM crash (X server reparents + it back to root). +12. **Map** — `XMapWindow()` frame (and client if appropriate). +13. **`XUngrabServer()`** — Release atomic lock. +14. **Notify** — `WMPostNotificationName(WMNManaged, ...)`. + +#### `wUnmanageWindow(WWindow *wwin, Bool restore, Bool destroyed)` — Release + +1. Close any open inspector/menu for this window. +2. `XRemoveFromSaveSet()` — No longer need crash protection. +3. `XSelectInput(client, NoEventMask)` — Stop receiving events. +4. `XUngrabButton()` / `XUngrabKey()` — Release all grabs. +5. `XUnmapWindow()` — Hide frame and client. +6. If `restore`: `wClientRestore()` — `XReparentWindow(client, root, x, y)` back + to root, `XSetWindowBorderWidth()` to original value. +7. Remove from focus list, determine next focused window. +8. Remove from workspace window list. +9. `WMPostNotificationName(WMNUnmanaged, ...)`. +10. `wWindowDestroy()` — Free all resources. + +--- + +### A1: Stacking (Raise/Lower) + +**File:** `src/stacking.c` + +**Roadmap ref:** Phase A, task A1 — "Stacking (raise/lower via scene graph)" + +#### Data Structure + +``` +scr->stacking_list = WMBag (sparse array indexed by window_level) + Level 0 (WMBackLevel): frame_a ↔ frame_b ↔ ... + Level 2 (WMNormalLevel): frame_c ↔ frame_d ↔ frame_e ↔ ... + Level 4 (WMFloatingLevel): frame_f ↔ ... + Level 6 (WMDockLevel): frame_g ↔ ... + ... +``` + +Each level is a doubly-linked list (`stacking->above` / `stacking->under`). +Head of list = topmost window at that level. + +#### Key Operations + +| Function | X11 Mechanism | Notes | +|----------|---------------|-------| +| `wRaiseFrame(frame)` | Unlink from list → relink at head → `CommitStackingForWindow()` | Recursively raises transients first | +| `wLowerFrame(frame)` | Unlink → relink at tail → `CommitStackingForWindow()` | Won't lower below owner if transient | +| `CommitStacking(scr)` | Flatten all levels top→bottom into `Window[]` → `XRestackWindows(dpy, wins, count)` | Full restack — expensive, used rarely | +| `CommitStackingForWindow(frame)` | Find window above in higher level → `XRestackWindows(dpy, {above, frame}, 2)` | Pairwise restack — O(1) | +| `ChangeStackingLevel(frame, level)` | Remove from old level → add to new → raise/lower | Used by fullscreen, dock level changes | +| `RemakeStackList(scr)` | `XQueryTree(root)` → rebuild internal list from server state | Recovery/sync | + +#### Transient Handling + +`stacking->child_of` links transients to owners. `wRaiseFrame` walks the transient +chain and raises each transient above its owner. `wLowerFrame` refuses to lower a +window below its owner. + +--- + +### A2: Maximize + +**File:** `src/actions.c` + +**Roadmap ref:** Phase A, task A2 — "Window maximize" + +#### `wMaximizeWindow(WWindow *wwin, int directions)` + +1. Save current geometry in `wwin->old_geometry`. +2. Compute target geometry based on direction flags: + - `MAX_VERTICAL` — expand height to usable area + - `MAX_HORIZONTAL` — expand width to usable area + - `MAX_LEFTHALF` / `MAX_RIGHTHALF` — half-screen tiling + - `MAX_TOPHALF` / `MAX_BOTTOMHALF` — vertical half tiling + - `MAX_MAXIMUS` — expand to fill gaps between other windows +3. Apply size constraints from `WM_NORMAL_HINTS` (min/max/increment). +4. `wWindowConfigure(wwin, x, y, w, h)` → `XMoveResizeWindow()`. +5. Set `wwin->flags.maximized` flags. +6. `wWindowSynthConfigureNotify()` — Send synthetic ConfigureNotify to client. + +#### `handleMaximize(WWindow *wwin, int directions)` + +Smart dispatcher handling: +- Toggle: if already maximized in same direction → `wUnmaximizeWindow()` +- Half-screen transitions: left↔right, top↔bottom +- Cross-head movement for multi-monitor +- Quarter tiling (top-left, top-right, bottom-left, bottom-right) + +#### `wUnmaximizeWindow(WWindow *wwin)` + +Restores `wwin->old_geometry`, clears maximize flags, reconfigures. + +--- + +### A3: Shade (Titlebar Collapse) + +**File:** `src/actions.c` + +**Roadmap ref:** Phase A, task A3 — "Window shade (titlebar collapse)" + +#### `wShadeWindow(WWindow *wwin)` + +1. `XSelectInput(client, NoEventMask)` — Temporarily suppress UnmapNotify. +2. `XUnmapWindow(client)` — Hide client content. +3. `XSelectInput(client, original_mask)` — Restore event mask. +4. `shade_animate()` — Incremental resize animation (frame shrinks to titlebar height). +5. `wFrameWindowResize(frame, width, top_width - 1)` — Final size = titlebar only. +6. Set `wwin->flags.shaded = 1`. +7. `wClientSetState(wwin, IconicState)` — Update WM_STATE property. + +#### `wUnshadeWindow(WWindow *wwin)` + +1. `shade_animate()` — Expand animation. +2. `wFrameWindowResize(frame, width, full_height)`. +3. `XMapWindow(client)` — Show client content again. +4. Set `wwin->flags.shaded = 0`. +5. `wClientSetState(wwin, NormalState)`. +6. `wSetFocusTo(scr, wwin)` — Refocus. + + +--- + +### A4: Workspace Switching + +**File:** `src/workspace.c` + +**Roadmap ref:** Phase A, task A4 — "Workspace switching (create/destroy/switch)" + +#### Data Structure + +```c +scr->workspaces[] // array of WWorkspace* (name, clip dock) +scr->workspace_count // current count +scr->current_workspace // active workspace index +``` + +Each `WWindow` has `wwin->frame->workspace` indicating which workspace it belongs to. +Windows with `wwin->window_flags.omnipresent` appear on all workspaces. + +#### `wWorkspaceForceChange(WScreen *scr, int workspace)` + +The core switching algorithm: + +1. Iterate `scr->focused_window` linked list (all managed windows). +2. For each window: + - If on old workspace and NOT omnipresent → `wWindowUnmap()` (calls `XUnmapWindow`) + - If on new workspace and NOT omnipresent → `wWindowMap()` (calls `XMapWindow`) +3. Update `scr->current_workspace`. +4. Determine focus target: + - Click-to-focus: find last focused window on new workspace + - Sloppy focus: `XQueryPointer()` → find window under pointer +5. `wSetFocusTo(scr, target)`. +6. `ProcessPendingEvents()` — Drain stale focus/crossing events. +7. `showWorkspaceName()` — Render fade-in/out name overlay. +8. `wArrangeIcons()` — Relayout icon yard. + +#### Workspace Name Display + +`showWorkspaceName()` renders text into an `RImage`, composites it with a +semi-transparent background, converts to Pixmap via `RConvertImage()`, applies +as window background with `XSetWindowBackgroundPixmap()`, and uses +`XShapeCombineMask()` for non-rectangular shape. A timer-based fade animation +updates the alpha blend each step. + +--- + +### A5: Alt-Tab Window Cycling + +**File:** `src/cycling.c` + +**Roadmap ref:** Phase A, task A5 — "Alt-Tab window cycling + switch panel" + +#### `StartWindozeCycle(WWindow *wwin, XEvent *event, Bool next, Bool class_only)` + +1. **`XGrabKeyboard(root, True, GrabModeAsync, ...)`** — Modal keyboard grab. +2. **`wInitSwitchPanel()`** — Create visual panel showing window thumbnails. +3. **Modal event loop** — `WMMaskEvent()` filtering KeyPress, KeyRelease, Expose, + MotionNotify, ButtonRelease: + - Tab / Right → select next window + - Shift+Tab / Left → select previous + - Home / End → first / last + - Escape → cancel (restore original focus) + - Return → confirm selection + - Modifier key release (Alt/Super) → confirm selection +4. Each selection: `wSwitchPanelSelectNext()` → `change_focus_and_raise()`: + - `wWindowFocus(wwin)` — Visual focus indication + - `raiseWindow()` — `XRestackWindows()` to show highlighted window +5. **On exit:** + - `XUngrabKeyboard()` + - Destroy switch panel + - `wRaiseFrame()` + `wSetFocusTo()` on final selection + +#### Key X11 Mechanisms + +- `XGrabKeyboard` — Intercepts ALL keyboard input during cycling +- `XKeysymToKeycode` — Converts keysyms for comparison in event loop +- `XRestackWindows` — Temporarily raises highlighted window below panel +- `WMMaskEvent` — Selective event retrieval (motion compression) + +--- + +### A6: Iconification (Minimize) + +**File:** `src/actions.c`, `src/icon.c` + +**Roadmap ref:** Phase A, task A6 — "Window iconification (minimize) + miniwindow" + +#### `wIconifyWindow(WWindow *wwin)` + +1. **Create icon** — `wwin->icon = wIconCreate(wwin)`: + - Allocates `WIcon` struct + - Creates icon X window via `wCoreCreateTopLevel()` + - Loads icon image (from hints, .desktop file, or default) + - Renders icon tile with wrlib +2. **Capture mini-preview** — `wWindowCaptureIcon(wwin)`: + - `XGetImage()` on client window → `RCreateImageFromXImage()` → scale to icon size +3. **Unmap window** — `XUnmapWindow(frame)` + unmap all transients. +4. **Animation** — `animateResize()` draws XOR wireframe zoom from window rect to + icon position (using `XGrabServer` + `XDrawRectangle` with frame_gc in GXxor mode). +5. **Place icon** — `PlaceIcon()` finds slot in icon yard. +6. **Map icon** — `XMapWindow(icon->core->window)`. +7. **Update state** — `wClientSetState(wwin, IconicState)`, set `flags.miniaturized`. +8. **Add to stacking** — `AddToStackList(icon->core)`. + +#### `wDeiconifyWindow(WWindow *wwin)` + +1. **Animation** — Reverse zoom from icon to window position. +2. **Map window** — `XMapWindow(frame)` + map transients. +3. **Destroy icon** — `RemoveFromStackList()`, `wIconDestroy()`. +4. **Raise & focus** — `wRaiseFrame()`, `wSetFocusTo()`. +5. **Update state** — `wClientSetState(wwin, NormalState)`, clear `flags.miniaturized`. + +--- + +### A7: Window Placement + +**File:** `src/placement.c` + +**Roadmap ref:** Phase A, task A7 — "Window placement algorithms (smart/cascade/random)" + +#### `PlaceWindow(WWindow *wwin, int *x_ret, int *y_ret, unsigned w, unsigned h)` + +Dispatches based on `wPreferences.window_placement`: + +| Mode | Algorithm | X11 Usage | +|------|-----------|-----------| +| `WPM_SMART` | Two-pass overlap minimization: coarse grid scan → fine-tune around best point | None (pure geometry) | +| `WPM_CASCADE` | Diagonal staircase offset from origin, wraps at screen edge | None | +| `WPM_CENTER` | Center of usable area for the target head | None | +| `WPM_AUTO` | Try center, then grid scan for non-overlapping slot, fallback to cascade | None | +| `WPM_RANDOM` | Random position within usable area | None | +| `WPM_MANUAL` | `InteractivePlaceWindow()` — user clicks to place | `XGrabPointer`, modal event loop | + +#### Smart Placement Detail + +`smartPlaceWindow()`: +1. Get usable area via `wGetUsableAreaForHead()` (accounts for dock, struts). +2. Coarse scan: step through positions at `PLACETEST_HSTEP`/`VSTEP` intervals. +3. At each position: `calcSumOfCoveredAreas()` iterates all mapped windows, + computing overlap area. +4. Track position with minimum total overlap. +5. Fine scan: search around best coarse position at pixel granularity. +6. Return best position (may have non-zero overlap if screen is full). + +#### Icon Placement + +`PlaceIcon(WScreen *scr, int *x_ret, int *y_ret, int head)`: +- Maintains a bitmap grid of occupied icon slots +- Scans for first free slot in configured direction (bottom-left, top-right, etc.) +- Grid cell size = icon tile size + padding + + +--- + +## 4. Phase B: Dock & App Icons + +**File:** `src/dock.c`, `src/appicon.c` + +**Roadmap ref:** Phase B — "Dock & App Icons" + +### Architecture + +``` +WDock (type: WM_DOCK | WM_CLIP | WM_DRAWER) + ├── icon_array[0] = main icon (dock tile / clip indicator) + ├── icon_array[1] = WAppIcon* (docked app) + ├── icon_array[2] = WAppIcon* (docked app) + └── ... +``` + +- **Dock** — Fixed vertical bar (one per screen), persists across workspaces +- **Clip** — One per workspace, shows workspace number + directional arrows +- **Drawer** — Horizontal extension of the dock + +### Key Operations + +| Operation | Implementation | X11 Mechanism | +|-----------|---------------|---------------| +| Create dock | `wDockCreate()` → `mainIconCreate()` | `wCoreCreateTopLevel()` → `XCreateWindow` | +| Paint icon | `dockIconPaint()` / `wClipIconPaint()` | wrlib render → `XSetWindowBackgroundPixmap` + `XClearWindow` | +| Launch app | `execCommand()` → `fork()` + `execvp()` | Process management (not X11) | +| Track death | `wAddDeathHandler()` → `buryChild()` | `waitpid(WNOHANG)` in SIGCHLD handler | +| Icon drag | `iconMouseDown()` modal loop | `XGrabPointer` + MotionNotify loop | +| Swap sides | `wDockSwap()` | `XMoveWindow` for each icon | +| Show/hide | `wDockShowIcons()` / `wDockHideIcons()` | `XMapWindow` / `XUnmapWindow` per icon | +| Clip arrows | `wClipIconPaint()` | `XDrawLines` for directional arrows | +| Auto-collapse | `clipAutoCollapse()` timer | `XUnmapWindow` non-kept icons after delay | +| DND awareness | `wXDNDMakeAwareness()` | `XChangeProperty` sets `XdndAware` atom | + +### Clip Workspace Switching + +The clip icon shows the workspace number and directional arrows. Clicking arrows +triggers `handleClipChangeWorkspace()` → `wWorkspaceRelativeChange()`. + +### Dock State Persistence + +`dock_state()` serializes the dock to a `WMPropList` (property list): +- Position, auto-raise, auto-collapse, lowered flags +- Per-icon: command, wm_instance, wm_class, position, auto-launch, lock flags + +Restored via `wDockRestoreState()` at startup. + +--- + +## 5. Phase C: Visual Polish & Animations + +**Files:** `src/actions.c`, `src/superfluous.c`, `src/framewin.c` + +**Roadmap ref:** Phase C — "Visual Polish & Animations" + +### Frame Decoration Rendering + +**File:** `src/framewin.c` + +The frame window is composed of sub-windows: + +``` +WFrameWindow (top-level X window) + ├── titlebar (child window, height ~19px) + │ ├── leftButton (close/miniaturize) + │ ├── rightButton (maximize/rollup) + │ └── [languageButton] (optional XKB indicator) + └── resizebar (child window at bottom, height ~8px) +``` + +#### Texture Rendering Pipeline + +1. `remakeTexture()` — For each state (focused, unfocused, pfocused): + - `wTextureRenderImage()` → wrlib renders gradient/pixmap/solid into `RImage` + - `renderTexture()` splits the image into button-width segments + - `RConvertImage()` → converts `RImage` to X `Pixmap` +2. `updateTexture()` — Applies pixmaps: + - `XSetWindowBackgroundPixmap(titlebar, pixmap)` + `XClearWindow()` + - Or `XSetWindowBackground(titlebar, pixel)` for solid colors +3. `wFrameWindowPaint()` — Draws text title (with justification), resizebar bevels + (`XDrawLine` for 3D effect), triggers button repaints. + +#### Button Rendering + +`paintButton()`: +- Normal: button background from texture + icon image via `XCopyArea`/`XCopyPlane` +- Pressed: `XFillRectangle` darkened + icon + `XDrawRectangle` border + +### Animations + +#### Iconify/Deiconify Zoom (`actions.c`) + +```c +animateResize(scr, x1, y1, w1, h1, x2, y2, w2, h2) +``` + +1. `XGrabServer()` — Prevent other drawing during animation. +2. Loop `MINIATURIZE_ANIMATION_STEPS` times: + - Interpolate rectangle from source to destination + - `XDrawRectangle(root, frame_gc)` — GC is in GXxor mode + - `wusleep()` delay + - `XDrawRectangle(root, frame_gc)` — XOR erases previous frame +3. `XUngrabServer()`. + +Variants: `animateResizeFlip` (3D flip), `animateResizeTwist` (rotation), +`animateResizeZoom` (expanding rectangles). + +#### Shade Animation (`actions.c`) + +`shade_animate()`: +- Incremental `wFrameWindowResize()` calls, stepping height by + `SHADE_STEPS` pixels per frame with `wusleep()` delays. + +#### Workspace Name Fade (`workspace.c`) + +- Pre-renders text into `RImage` with semi-transparent background +- Timer callback (`hideWorkspaceName`) blends toward transparent each tick +- Each frame: `RConvertImage()` → `XSetWindowBackgroundPixmap()` → `XClearWindow()` +- Uses `XShapeCombineMask()` for non-rectangular window shape + +### Balloon Tooltips + +**File:** `src/balloon.c` + +- Creates an override-redirect X window +- Renders text with wrlib into shaped window +- `XShapeCombineMask()` for rounded balloon shape +- Positioned near the target (dock icon, titlebar button) +- Timer-based show/hide with delay + + +--- + +## 6. Phase G: Session & Persistence + +**Files:** `src/session.c`, `src/defaults.c`, `src/screen.c` + +**Roadmap ref:** Phase G — "Session & Persistence" + +### Defaults Database + +- Location: `~/GNUstep/Defaults/WindowMaker`, `WMRootMenu`, `WMWindowAttributes` +- Format: NeXTSTEP property lists (parsed by WINGs `WMPropList`) +- Read at startup via `wReadDefaults()` / `wReadStaticDefaults()` +- Monitored via inotify for live reload (`handle_inotify_events()` in event.c) +- **Zero X11 dependency** — pure file I/O + proplist parsing + +### Session Save (`session.c`) + +`wSessionSaveState(WScreen *scr)`: +- Iterates all managed windows +- For each: saves geometry, workspace, shaded/miniaturized/hidden state, + WM_CLASS, WM_COMMAND (for restart) +- Saves dock/clip/drawer state +- Writes to `~/GNUstep/Defaults/WMState` + +### Session Restore + +`wScreenRestoreState(WScreen *scr)`: +- Reads `WMState` file +- Restores workspace count and names +- Restores dock state (`wDockRestoreState`) +- Restores clip per workspace +- Restores drawers + +At `manageAllWindows()` time, windows are matched by WM_CLASS to saved state +and positioned/stated accordingly. + +### Dock State Persistence + +Each dock icon stores: command, wm_instance, wm_class, position in dock, +auto-launch flag, lock flag. Serialized as nested property lists in WMState. + +--- + +## 7. Phase I: EWMH Compliance + +**File:** `src/wmspec.c` + +**Roadmap ref:** Phase I — "EWMH / Foreign Toplevel" + +### Initialization (`wNETWMInitStuff`) + +Sets root window properties: +- `_NET_SUPPORTED` — List of all supported EWMH atoms +- `_NET_SUPPORTING_WM_CHECK` — Points to a child window with `_NET_WM_NAME` +- `_NET_NUMBER_OF_DESKTOPS`, `_NET_CURRENT_DESKTOP` +- `_NET_DESKTOP_GEOMETRY`, `_NET_DESKTOP_VIEWPORT` +- `_NET_WORKAREA` — Usable area per workspace (accounts for struts) + +### Per-Window Properties + +| Property | When Set | Source | +|----------|----------|--------| +| `_NET_WM_STATE` | On manage, on state change | `wNETWMUpdateState()` | +| `_NET_WM_DESKTOP` | On manage, on workspace change | `wNETWMUpdateDesktop()` | +| `_NET_WM_ALLOWED_ACTIONS` | On manage | `wNETWMUpdateActions()` | +| `_NET_FRAME_EXTENTS` | On manage, on border change | `wNETWMUpdateFrameExtents()` | + +### Client Messages Handled + +| Message | Action | +|---------|--------| +| `_NET_WM_STATE` | Toggle/set maximized, fullscreen, above, below, sticky, skip-taskbar, shaded | +| `_NET_CLOSE_WINDOW` | `wClientSendProtocol(WM_DELETE_WINDOW)` | +| `_NET_MOVERESIZE_WINDOW` | `wWindowConfigure()` with gravity | +| `_NET_WM_MOVERESIZE` | Start interactive move/resize | +| `_NET_ACTIVE_WINDOW` | Focus + raise the window | +| `_NET_CURRENT_DESKTOP` | `wWorkspaceChange()` | +| `_NET_WM_DESKTOP` | Move window to workspace | +| `_NET_RESTACK_WINDOW` | `wRaiseFrame()` / `wLowerFrame()` | + +### Strut Handling + +`_NET_WM_STRUT_PARTIAL` / `_NET_WM_STRUT` properties on client windows reserve +screen edges. `wScreenUpdateUsableArea()` collects all struts and computes the +usable area per head, which is used by placement, maximize, and icon yard. + +--- + +## 8. X11 Mechanisms Summary + +This section catalogs the X11 mechanisms used by each feature, providing the +definitive reference for what the Wayland port must replace. + +### Core X11 Primitives Used + +| X11 Mechanism | WM Usage | Wayland Equivalent | +|---------------|----------|-------------------| +| `SubstructureRedirectMask` on root | Claim WM role; intercept Map/Configure requests | `xdg_shell::new_surface` signal | +| `XReparentWindow` | Embed client in frame | Scene graph parent (conceptual) | +| `XRestackWindows` | Z-order management | `wlr_scene_node_raise/lower` | +| `XSetInputFocus` | Keyboard focus delivery | `wlr_seat_keyboard_notify_enter` | +| `XGrabServer` | Atomic multi-window operations | No equivalent (async model) | +| `XGrabKeyboard` | Modal keyboard capture (alt-tab) | `wlr_seat_keyboard_start_grab` | +| `XGrabPointer` | Modal pointer capture (move/resize) | `wlr_seat_pointer_start_grab` | +| `XSaveContext/XFindContext` | Window ID → object lookup | Hash table | +| `XAddToSaveSet` | Crash recovery | No equivalent (clients die with compositor) | +| `XGetWindowAttributes` | Read client geometry/state | `wlr_xdg_toplevel` fields | +| `XGetWMHints/XGetTransientForHint` | Read client hints | `wlr_xdg_toplevel` / `app_id` | +| `XChangeProperty/XGetWindowProperty` | EWMH, WM_STATE, atoms | `ext-foreign-toplevel-list` | +| `XInternAtom` | Named constants | Not needed (protocols are typed) | +| `XMapWindow/XUnmapWindow` | Show/hide windows | `wlr_scene_node_set_enabled` | +| `XMoveResizeWindow` | Position/size changes | `wlr_scene_node_set_position` + configure | +| `XCreateWindow` | Frame, internal windows | `wlr_scene_tree` / `wlr_scene_buffer` | +| `XSetWindowBackgroundPixmap` | Decoration rendering | `wlr_scene_buffer_set_buffer` | +| `XDrawLine/XDrawRectangle` (XOR GC) | Wireframe animations | Pixman overlay buffer per frame | +| `XGetImage` | Icon mini-preview capture | Last committed `wlr_texture` blit | +| `XShapeCombineMask` | Non-rectangular windows | Alpha channel in buffer | +| `XQueryTree` | Enumerate existing windows | Not needed (compositor sees all surfaces) | +| `XSendEvent` (ClientMessage) | ICCCM/EWMH communication | `xdg_toplevel_send_close` etc. | +| `XSelectInput` | Per-window event subscription | Signal-based (no subscription needed) | +| `WM_Sn` selection | WM replacement protocol | Not applicable | + +### Per-Feature X11 Call Inventory + +| Feature (Roadmap Phase) | Primary X11 Calls | File(s) | +|------------------------|-------------------|---------| +| **A1: Stacking** | `XRestackWindows`, `XRaiseWindow`, `XLowerWindow`, `XQueryTree` | stacking.c | +| **A2: Maximize** | `XMoveResizeWindow`, `XSendEvent` (ConfigureNotify) | actions.c | +| **A3: Shade** | `XUnmapWindow`/`XMapWindow` (client), `XResizeWindow` (frame) | actions.c | +| **A4: Workspace** | `XMapWindow`/`XUnmapWindow` (per window), `XQueryPointer` | workspace.c | +| **A5: Alt-Tab** | `XGrabKeyboard`/`XUngrabKeyboard`, `XRestackWindows` | cycling.c | +| **A6: Iconify** | `XGetImage`, `XUnmapWindow`, `XGrabServer`, `XDrawRectangle` (XOR) | actions.c, icon.c | +| **A7: Placement** | `XGrabPointer` (manual mode only) | placement.c | +| **B: Dock** | `XCreateWindow`, `XMoveWindow`, `XMapWindow`, `XDrawLines` | dock.c, appicon.c | +| **C: Animations** | `XGrabServer`, `XDrawRectangle`/`XDrawLines` (XOR GC) | actions.c | +| **C: Decorations** | `XSetWindowBackgroundPixmap`, `XClearWindow`, `XDrawLine`, `XCopyArea` | framewin.c | +| **G: Session** | None (pure file I/O) | session.c | +| **I: EWMH** | `XChangeProperty`, `XGetWindowProperty`, `XInternAtom`, `XSendEvent` | wmspec.c | + +### Focus Model Implementation + +**File:** `src/actions.c` — `wSetFocusTo(WScreen *scr, WWindow *wwin)` + +Implements ICCCM focus models: + +| Client Type | Detection | WM Action | +|-------------|-----------|-----------| +| Passive (input=True, no TAKE_FOCUS) | `WM_HINTS.input = True` | `XSetInputFocus(client, RevertToParent, timestamp)` | +| Locally active (input=True + TAKE_FOCUS) | Both flags set | `XSetInputFocus()` + send `WM_TAKE_FOCUS` ClientMessage | +| Globally active (input=False + TAKE_FOCUS) | `WM_HINTS.input = False` | Send `WM_TAKE_FOCUS` only (client calls `XSetInputFocus` itself) | +| No input (input=False, no TAKE_FOCUS) | Neither flag | `XSetInputFocus(no_focus_win)` — focus goes to fallback | + +Focus changes also trigger: +- `wWindowFocus(wwin)` / `wWindowUnfocus(old)` — visual state update +- `wFrameWindowChangeState()` — repaint titlebar in focused/unfocused texture +- Colormap installation — `XInstallColormap(wwin->cmap_windows[0])` +- App menu activation — `wAppMenuMap(wwin->main_window_desc)` + +### Interactive Grab Pattern + +All interactive operations (move, resize, alt-tab) follow the same pattern: + +``` +1. XGrabPointer/XGrabKeyboard — exclusive input capture +2. Modal event loop: + WMMaskEvent(mask, &event) — selective event retrieval + switch(event.type): + MotionNotify → update position/state + KeyPress → handle navigation + ButtonRelease → confirm +3. XUngrabPointer/XUngrabKeyboard — release +4. Apply final state +``` + +This pattern is used in: +- `wMouseMoveWindow()` — pointer grab, motion loop +- `wKeyboardMoveResizeWindow()` — keyboard grab, key loop +- `StartWindozeCycle()` — keyboard grab, key loop +- `InteractivePlaceWindow()` — pointer grab, click to place + +--- + +## Cross-Reference: Roadmap Phase → Source File → X11 Mechanism + +| Phase | Feature | Source File | Core X11 Mechanism | Complexity for Wayland | +|-------|---------|-------------|-------------------|----------------------| +| A1 | Stacking | stacking.c | `XRestackWindows` | Low — scene graph is simpler | +| A2 | Maximize | actions.c | `XMoveResizeWindow` | Low — geometry is policy-side | +| A3 | Shade | actions.c | `XUnmapWindow` + `XResizeWindow` | Low — hide client node + resize frame | +| A4 | Workspace | workspace.c | `XMapWindow`/`XUnmapWindow` per window | Medium — scene tree toggle is O(1) | +| A5 | Alt-Tab | cycling.c | `XGrabKeyboard` + modal loop | Medium — compositor-internal grab | +| A6 | Iconify | actions.c, icon.c | `XGetImage` + `XGrabServer` + XOR draw | Medium — no XOR, need pixman overlay | +| A7 | Placement | placement.c | Pure geometry (no X11 except manual) | Low — already portable | +| B | Dock | dock.c | `XCreateWindow` + `XMoveWindow` | Medium — scene buffers instead | +| C | Animations | actions.c | `XGrabServer` + XOR `XDrawRectangle` | High — must rewrite as per-frame renders | +| C | Decorations | framewin.c | `XSetWindowBackgroundPixmap` | Medium — pixman → scene buffer | +| G | Session | session.c | None | None — already portable | +| I | EWMH | wmspec.c | `XChangeProperty`/`XGetWindowProperty` | N/A — EWMH is X11-only; use foreign-toplevel | + + +--- + +## Appendix A: Interactive Move/Resize Detail + +**File:** `src/moveres.c` + +**Roadmap ref:** Implicit in Phase A (core WM interaction) + +### `wMouseMoveWindow(WWindow *wwin, XEvent *ev)` + +The most complex interactive operation in the WM: + +1. **Setup:** + - `XGrabPointer(root, PointerMotionMask|ButtonReleaseMask, cursor_move)` + - Build sorted edge lists from all visible windows (for edge resistance) + - Show geometry display widget + +2. **Modal loop** (`WMMaskEvent`): + - **MotionNotify**: `updateWindowPosition()` with edge resistance: + - Binary search sorted edge lists for nearby edges + - Apply resistance force (configurable pixel threshold) + - Snap to screen edges/corners + - Handle edge wrapping (pointer warps to opposite edge) + - **KeyPress**: Escape cancels, Return confirms + - **ButtonRelease**: Confirms final position + +3. **Modes:** + - **Opaque**: `XMoveWindow()` on every motion event (live feedback) + - **Wireframe**: `XGrabServer()` + `XDrawRectangle(root, frame_gc)` in XOR mode + +4. **Cleanup:** + - `XUngrabPointer()` + - Apply final position via `wWindowConfigure()` + - Destroy geometry display + +### Edge Resistance Algorithm + +`updateWindowPosition()`: +- Maintains 4 sorted arrays: `leftList`, `rightList`, `topList`, `bottomList` +- Each contains edges of all visible windows + screen boundaries +- On each motion: binary search for edges within resistance threshold +- If found: snap proposed position to the edge (sticky behavior) +- Configurable: `wPreferences.edge_resistance` (pixels) + +### Keyboard Move/Resize + +`wKeyboardMoveResizeWindow()`: +- `XGrabKeyboard()` instead of pointer +- Arrow keys move (or resize with Shift held) +- Step size: 1px normally, 10px with Ctrl + +--- + +## Appendix B: Menu System + +**File:** `src/menu.c`, `src/rootmenu.c` + +**Roadmap ref:** Implicit in Phase A/E + +### Menu Architecture + +``` +WMenu + ├── frame (WCoreWindow — the menu window itself) + ├── entries[] (WMenuEntry — label, shortcut, callback, submenu) + └── brother (WMenu* — shared submenu instance for cascading) +``` + +Each menu is an override-redirect X window. Submenus are separate windows +positioned adjacent to the parent entry. + +### Key Operations + +| Operation | X11 Mechanism | +|-----------|---------------| +| Show menu | `XMapRaised(menu->frame->core->window)` | +| Hide menu | `XUnmapWindow()` | +| Paint entry | `XClearArea()` + `wDrawString()` (wrlib text) | +| Highlight | `XFillRectangle()` with highlight color | +| Keyboard nav | `XGrabKeyboard()` + key event loop | +| Scroll (long menus) | `XMoveWindow()` to scroll menu window | + +### Root Menu + +`wRootMenuPerformShortcut()` handles global keyboard shortcuts by matching +keycode+modifiers against the menu tree's shortcut bindings. + +--- + +## Appendix C: Data Structure Relationships + +``` +WScreen (per X screen) + ├── root_win (Window — the X root window) + ├── focused_window → WWindow → WWindow → ... (linked list, MRU order) + ├── stacking_list (WMBag — per-level linked lists of WCoreWindow) + ├── workspaces[] (WWorkspace — name, clip) + ├── current_workspace (int) + ├── dock (WDock* — the main dock) + ├── rcontext (RContext* — wrlib rendering context) + ├── w_win (WMScreen* — WINGs screen) + ├── GCs: frame_gc, line_gc, copy_gc, stipple_gc, ... + └── internal windows: no_focus_win, dock_shadow, workspace_name + +WWindow (per managed client) + ├── client_win (Window — the actual client X window) + ├── frame (WFrameWindow* — decoration frame) + ├── icon (WIcon* — miniwindow when iconified) + ├── main_window_desc (WWindow* — app group leader) + ├── transient_for (Window — owner if transient) + ├── prev/next (linked list through focused_window) + ├── flags: maximized, shaded, miniaturized, hidden, fullscreen, focused, ... + ├── old_geometry (saved for unmaximize) + ├── normal_hints (WM_NORMAL_HINTS — size constraints) + └── wm_hints, wm_class, protocols, ... + +WFrameWindow (decoration) + ├── core (WCoreWindow — the frame X window) + ├── titlebar (WCoreWindow — child window) + ├── left_button, right_button (WCoreWindow — child windows) + ├── resizebar (WCoreWindow — child window) + ├── title (char* — window title text) + ├── textures: focused, unfocused, pfocused (Pixmap per state) + └── callbacks: on_click_left, on_dblclick_titlebar, on_mousedown_resizebar, ... + +WCoreWindow (lowest-level X window wrapper) + ├── window (Window — the X window ID) + ├── descriptor (WObjDescriptor — event dispatch target) + ├── stacking (WStacking — above/under/level/child_of) + └── width, height +``` + +--- + +## Appendix D: Key Invariants for Wayland Port Verification + +These invariants hold in the X11 implementation and must be preserved: + +1. **Reparenting semantics**: After `wManageWindow`, the client window is a child + of the frame window. All geometry reported to the client accounts for frame borders. + +2. **Focus follows manage order**: The `focused_window` linked list is in MRU order. + `wSetFocusTo` moves the target to the head. + +3. **Stacking is per-level**: A window at `WMNormalLevel` can never appear above a + window at `WMFloatingLevel`, regardless of raise/lower operations within a level. + +4. **Transients stack above owners**: `wRaiseFrame` recursively raises transients. + A transient can never be below its owner within the same level. + +5. **Workspace membership is exclusive**: A window is on exactly one workspace + (unless omnipresent). Switching workspaces maps/unmaps entire sets atomically. + +6. **Maximize preserves old geometry**: `old_geometry` is saved before maximize and + restored on unmaximize. Nested maximize operations (e.g., vertical then horizontal) + accumulate flags but save geometry only on first maximize. + +7. **Shade preserves client state**: The client window is unmapped but not destroyed. + Its geometry within the frame is unchanged. Unshade simply remaps it. + +8. **Icon placement is grid-based**: Icons occupy fixed-size slots. The bitmap + prevents overlap. Slots are reclaimed on deiconify. + +9. **Edge resistance is symmetric**: Both window-to-window and window-to-screen-edge + resistance use the same threshold and algorithm. + +10. **EWMH state is authoritative**: The WM writes `_NET_WM_STATE` and it reflects + actual window state. Clients request changes via ClientMessage; the WM decides. diff --git a/src/appicon.c b/src/appicon.c index eb44fd8..e10cb4a 100644 --- a/src/appicon.c +++ b/src/appicon.c @@ -368,7 +368,7 @@ static void updateDockNumbers(WScreen *scr) snprintf(ws_numbers, sizeof(ws_numbers), "%i [ %i ]", scr->current_workspace + 1, ((scr->current_workspace / 10) + 1)); length = strlen(ws_numbers); - wm_backend->window_clear(dicon->icon->core->window); + wm_backend->window_clear_area(dicon->icon->core->window, 2, 2, 50, 50); WMDrawString(scr->wmscreen, dicon->icon->core->window, scr->black, scr->icon_title_font, 4, 3, ws_numbers, length); diff --git a/src/event.c b/src/event.c index 525ae3f..abdc362 100644 --- a/src/event.c +++ b/src/event.c @@ -779,8 +779,8 @@ static void handleUnmapNotify(WMEvent * event) /* whether the event is a Withdrawal request */ if (event->u.lifecycle.event == wwin->screen_ptr->root_win) { - XEvent *xev = (XEvent *)event->_x11_raw; - if (xev && xev->xunmap.send_event) +WMEvent *xev = event; + if (xev && xev->u.unmap.send_event) withdraw = True; }