482 lines
13 KiB
Markdown
482 lines
13 KiB
Markdown
# 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.
|