# WindowMaker Multi-Monitor Implementation Plan ## Purpose Implement multi-monitor support in a way that another coding agent can execute with minimal guesswork. This plan is written for the current `wmaker` tree and assumes: - X11 and Wayland backends both remain supported - shared policy code should continue to use the existing `head` abstraction - the initial goal is **correct multi-monitor geometry and behavior**, not a redesign of workspaces ## Scope ### In scope - Populate `WScreen.heads[]`, `head_count`, and `primary_head` correctly on both backends - Make Wayland support multiple outputs instead of only one - Recompute monitor geometry on output changes - Keep placement, maximize, fullscreen, icons, dock reservations, and menus head-aware - Handle output hotplug without leaving windows off-screen - Preserve existing single-monitor behavior ### Out of scope for phase 1 - Per-monitor workspaces - Per-monitor dock/clip instances - Full mixed-DPI polish beyond basic correctness --- ## Existing architecture to preserve ### Shared abstraction The core already has a backend-neutral monitor/head abstraction. Relevant files: - `src/screen.h` - `src/xinerama.h` - `src/xinerama.c` Important fields in `WScreen`: - `heads` - `head_count` - `primary_head` - `usableArea` - `totalUsableArea` Important helper APIs: - `wGetHeadForRect` - `wGetHeadForWindow` - `wGetHeadRelativeToCurrentHead` - `wGetHeadForPoint` - `wGetHeadForPointerLocation` - `wGetRectForHead` - `wGetUsableAreaForHead` - `wGetPointToCenterRectInHead` ### Existing backend support #### X11 - Monitor enumeration already exists in: - `src/backend/x11/x11_monitor.c` - Uses Xinerama with RandR screen-change subscriptions #### Wayland - Output layout support already exists via wlroots: - `src/backend/wayland/wl_backend.c` - `src/backend/wayland/wl_output.c` - `src/backend/wayland/wl_screen.c` - Current limitation: - `wl_screen.c` populates only `heads[0]` - some Wayland code assumes `usableArea[0]` --- ## Non-goals and policy choices ### Workspace policy Do **not** implement per-monitor workspaces in this effort. Use this policy instead: - a workspace still spans the full virtual desktop - placement/maximize/focus/icon logic remains **head-aware within that workspace** This keeps the patch set tractable and aligns with the current helper model. ### Dock / Clip policy For now: - keep one logical Dock and one logical Clip model - make their geometry reservations and placement logic head-aware Do not introduce per-head docks in this plan. --- ## Canonical monitor contract Before changing behavior, normalize the meaning of `heads[]`. ### Required invariants 1. `scr->heads` contains monitor rectangles in **global root coordinates**. 2. `scr->head_count >= 1` after screen initialization succeeds. 3. `scr->primary_head` is always a valid index when `head_count > 0`. 4. `scr->scr_width` / `scr->scr_height` represent the bounding box of the full virtual desktop. 5. `wGetRectForHead(scr, i)` must always return a valid monitor rectangle for `0 <= i < head_count`. 6. Single-monitor mode should still behave correctly. ### Preferred normalization Use **one explicit head even in single-monitor mode**. That means avoiding the old pattern where `head_count == 0` implies fallback-to-single-head behavior. If changing all callers at once is too risky, do this in two steps: 1. preserve compatibility in helpers 2. gradually move callers to assume `head_count >= 1` --- ## Execution plan ## Phase 1 — Audit and normalize head initialization ### Goal Make head data reliable and backend-neutral before extending Wayland. ### Files - `src/screen.h` - `src/xinerama.h` - `src/xinerama.c` - `src/screen.c` - `src/backend/x11/x11_monitor.c` ### Tasks 1. Audit all places that read: - `scr->heads` - `scr->head_count` - `scr->primary_head` 2. Decide whether to enforce `head_count >= 1` immediately or via compatibility layer. 3. Ensure the X11 path populates a valid single fallback head when Xinerama is inactive. 4. Ensure head arrays are allocated/freed consistently. 5. Add comments documenting the head contract. ### Acceptance criteria - X11 single-monitor behavior is unchanged. - There is a clear contract for `head_count`, `heads`, and `primary_head`. - Helper functions behave correctly with one or many heads. --- ## Phase 2 — Implement Wayland monitor enumeration ### Goal Populate all heads from `wlr_output_layout`, not just one. ### Files - `src/backend/wayland/wl_screen.c` - `src/backend/wayland/wl_output.c` - `src/backend/wayland/wl_backend.c` - `src/backend/backend_types.h` ### Tasks 1. Audit the Wayland global state for available outputs and layout access. 2. In `wl_screen_open()` or the appropriate geometry refresh path: - enumerate all active outputs - query each output's box via `wlr_output_layout_get_box` - build a `WHeadGeometry[]` 3. Set: - `scr->heads` - `scr->head_count` - `scr->primary_head` 4. Set `scr->scr_width` and `scr->scr_height` from the **union of all head rectangles**, not from a single output. 5. Preserve deterministic ordering of heads. - Recommended: sort by `(x, y)` or preserve wlroots layout iteration order if stable. 6. Choose primary head policy. - Recommended initial policy: first active output or currently selected backend primary output. ### Acceptance criteria - On Wayland with two outputs, `scr->head_count == 2`. - `wGetRectForHead()` returns both outputs with correct geometry. - `scr->scr_width` / `scr->scr_height` reflect the virtual desktop bounds. ### Notes for the implementing agent - Keep this backend-only. - Do not add special placement logic here. - Just make monitor geometry correct and complete. --- ## Phase 3 — Add dynamic output refresh / hotplug support ### Goal Rebuild monitor geometry whenever outputs change. ### Files - `src/backend/wayland/wl_output.c` - `src/backend/wayland/wl_screen.c` - `src/screen.c` - optionally `src/window.c`, `src/dock.c` ### Tasks 1. Identify Wayland output lifecycle hooks: - new output - destroyed/removed output - layout or mode change 2. Add a shared helper to rebuild screen head data. - Example helper name: `wl_screen_refresh_heads(scr)` 3. Recompute after each relevant event: - heads - virtual bounds - usable areas 4. If an output disappears, repair windows/icons that now lie outside all heads. ### Suggested repair policy For each affected window: 1. find nearest surviving head 2. preserve relative position when possible 3. clamp inside that head if necessary 4. fall back to centering in `primary_head` For icons/appicons: - do the same, but prefer the head they touched before removal ### Acceptance criteria - Plugging/unplugging outputs does not leave windows permanently off-screen. - Head helpers reflect the new output topology immediately. --- ## Phase 4 — Make usable-area and reservations per-head ### Goal Ensure placement/maximize respects per-head reserved areas. ### Files - `src/xinerama.c` - `src/screen.c` - `src/dock.c` - `src/backend/wayland/wl_dock.c` ### Known problem to fix Wayland dock code currently assumes `scr->usableArea[0]` in places. ### Tasks 1. Audit all reads/writes of: - `scr->usableArea` - `scr->totalUsableArea` - `scr->reservedAreas` 2. Ensure these arrays are sized for all heads. 3. Ensure dock/clip reservations affect the correct head. 4. When monitor geometry changes, recompute all per-head usable areas. 5. Remove hardcoded `[0]` assumptions. ### Acceptance criteria - Docks and clips reserve space only on the relevant head. - Maximize avoids reserved space on the current head. - Single-monitor behavior remains unchanged. --- ## Phase 5 — Audit all head-aware placement and movement paths ### Goal Make all geometry-sensitive actions consistent across monitors. ### Files to audit - `src/placement.c` - `src/window.c` - `src/actions.c` - `src/cycling.c` - `src/rootmenu.c` - `src/dialog.c` - `src/dock.c` ### Tasks Audit and fix the following behaviors: #### New window placement - New windows should open on: - the pointer head, or - the owner/transient head, or - the current head policy already used by helpers #### Restore/session placement - Restored geometry should be clamped or translated to a valid head. #### Maximize/fullscreen - Maximize should apply to the window's current head unless multi-head fullscreen is explicitly requested. - Existing fullscreen-monitor helpers in `actions.c` should keep working. #### Menus/dialogs/transients - Menus should open on the head that contains the invocation point. - Transients should prefer the owner head. - Dialog centering should use the correct head. #### Icons / miniwindows - Icon placement should remain on the window's head. - Auto-arrange should consider all heads, not just head 0. ### Acceptance criteria - Placement/maximize/fullscreen/menus/transients/icons all behave correctly on two monitors. --- ## Phase 6 — Add explicit monitor navigation commands ### Goal Expose useful multi-monitor actions to users. ### Files - `src/actions.c` - `src/defaults.c` - `WPrefs.app/KeyboardShortcuts.c` - possibly menu/defaults docs ### Tasks 1. Audit existing actions using: - `wGetHeadRelativeToCurrentHead` - `wGetHeadForWindow` 2. Finish or add commands such as: - move window to next monitor - move window to previous monitor - move focus to next monitor - maximize on current monitor 3. Expose them through defaults and WPrefs if not already exposed. ### Acceptance criteria - A user can move windows between monitors with shortcuts. - The actions behave correctly on X11 and Wayland. --- ## Phase 7 — Mixed scale / DPI follow-up (optional but recommended) ### Goal Avoid rendering problems on mixed-scale Wayland outputs. ### Files - `src/backend/wayland/wl_output.c` - `src/backend/wayland/wl_screen.c` - WINGs font/render paths ### Tasks 1. Track output scale per output/head. 2. Set output scale in wlroots where appropriate. 3. Feed DPI/scale into font/layout code. 4. Validate titlebars, menus, icons, screenshots, and WINGs rendering. ### Acceptance criteria - Multi-output works correctly even when outputs have different scales. --- ## Concrete code audit checklist Use this as a literal checklist while implementing. ### Shared monitor/head code - [ ] `src/xinerama.c` - [ ] `src/xinerama.h` - [ ] `src/screen.c` - [ ] `src/screen.h` ### X11 backend - [ ] `src/backend/x11/x11_monitor.c` ### Wayland backend - [ ] `src/backend/wayland/wl_backend.c` - [ ] `src/backend/wayland/wl_output.c` - [ ] `src/backend/wayland/wl_screen.c` - [ ] `src/backend/wayland/wl_dock.c` ### Placement/window policy - [ ] `src/placement.c` - [ ] `src/window.c` - [ ] `src/actions.c` - [ ] `src/cycling.c` - [ ] `src/rootmenu.c` - [ ] `src/dialog.c` - [ ] `src/dock.c` ### User-facing configuration - [ ] `src/defaults.c` - [ ] `WPrefs.app/KeyboardShortcuts.c` - [ ] docs / release notes --- ## Suggested implementation order for an agent If working incrementally, use this order: 1. **Normalize head contract** 2. **Enumerate all Wayland outputs into `heads[]`** 3. **Set virtual desktop bounds correctly** 4. **Recompute heads on hotplug / output change** 5. **Fix per-head usable area and dock reservations** 6. **Audit maximize/placement/fullscreen** 7. **Audit menus/transients/icons** 8. **Add monitor-navigation actions** 9. **Do scale/DPI follow-up** Do not start with user-facing shortcuts or DPI. Start with geometry correctness. --- ## Test matrix ## X11 tests - [ ] single monitor - [ ] dual monitor horizontal - [ ] dual monitor vertical - [ ] mixed resolutions - [ ] RandR hotplug/unplug - [ ] maximize on each head - [ ] fullscreen on each head - [ ] menu placement - [ ] dialog placement - [ ] icon placement ## Wayland tests Use nested mode if needed. Suggested setup: - `WMAKER_WAYLAND_NESTED=1` - adjust outputs with `wlr-randr` Run: - [ ] single output - [ ] two outputs horizontal - [ ] two outputs vertical - [ ] asymmetric offset layout - [ ] output removal with windows present - [ ] output addition after startup - [ ] maximize/fullscreen correctness - [ ] dock/clip reservation correctness - [ ] pointer-head placement correctness - [ ] WINGs app placement correctness - [ ] XWayland client placement correctness --- ## Debugging guidance for the implementing agent When behavior is wrong, log these values first: - `scr->head_count` - all `scr->heads[i]` - `scr->primary_head` - `scr->scr_width`, `scr->scr_height` - result of `wGetHeadForWindow()` for the affected window - result of `wGetHeadForPointerLocation()` during placement - per-head `usableArea` and `totalUsableArea` For Wayland output changes, log: - output add/remove - output layout box for each output - geometry refresh entry/exit - window migration decisions on output removal --- ## Definition of done This effort is complete when all of the following are true: - `heads[]` is correct on X11 and Wayland - Wayland supports more than one output - output hotplug updates geometry correctly - windows never remain stranded off-screen after output removal - maximize/fullscreen/placement/icons/menus are head-aware - dock/clip reservations are per-head - single-monitor behavior is unchanged --- ## Minimal deliverable if time is limited If this must be split across multiple PRs, the **minimum useful first PR** is: 1. normalize monitor contract 2. populate all Wayland heads 3. set correct virtual desktop bounds 4. fix obvious `usableArea[0]` assumptions That alone provides the base needed for all follow-up work.