windowmaker-wl/MULTI_MONITOR_IMPLEMENTATIO...

13 KiB

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.

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.