From 288d2ad3a724812f5f99beea45b3c733ac48e9aa Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Thu, 14 May 2026 11:57:15 +0200 Subject: [PATCH] Phase 82: wlroots 0.19 compatibility + native Wayland client support - Add wlr_subcompositor global (required by all modern Wayland clients) - Switch from xdg_shell new_surface to new_toplevel signal (wlroots 0.19 fires new_surface with role=NONE before get_toplevel assigns the role) - Defer initial xdg_surface configure to first client commit via surface->events.commit listener (surface must be initialized first) - Defer xdg_decoration set_mode to first commit (set_mode internally calls schedule_configure which asserts initialized) - Wire destroy_listener to toplevel->events.destroy instead of xdg_surface->events.destroy (wlroots asserts toplevel event lists are empty before the toplevel is freed) - Log DISPLAY and WAYLAND_DISPLAY env vars at startup - Add WAYLAND_ROADMAP.md Native Wayland clients (foot, etc.) can now connect, render, and disconnect without crashing the compositor. --- WAYLAND_ROADMAP.md | 245 +++++++++++++++++++++++++++++++ src/backend/wayland/wl_backend.c | 143 ++++++++++++------ 2 files changed, 343 insertions(+), 45 deletions(-) create mode 100644 WAYLAND_ROADMAP.md diff --git a/WAYLAND_ROADMAP.md b/WAYLAND_ROADMAP.md new file mode 100644 index 0000000..306837e --- /dev/null +++ b/WAYLAND_ROADMAP.md @@ -0,0 +1,245 @@ +# WindowMaker Wayland Port — Roadmap + +Last updated: 2026-05-14 + +## Architecture + +WindowMaker 0.96.0 uses a vtable-based backend abstraction (`WMBackend` struct in +`src/backend/backend.h`) with ~150+ function pointer slots. Policy code calls +`wm_backend->operation(...)` and never touches X11/Wayland APIs directly. + +Two backends: +- **X11**: `src/backend/x11/x11_backend.c` (150K) +- **Wayland**: `src/backend/wayland/wl_backend.c` (316K, wlroots-based compositor) + +Activation: `WMAKER_USE_WAYLAND=1` env var. Nested mode: `WMAKER_WAYLAND_NESTED=1`. + +Three major libraries: +- **src/** — WM core (policy layer, zero Xlib calls remaining) +- **WINGs/** — Widget toolkit ("WINGs Is Not GNUstep"), split into `_x11.c` / `_wl.c` backends +- **wrlib/** — Raster graphics library (image loading, manipulation, rendering) + +--- + +## Completed (as of 2026-05-14) + +- Backend vtable abstraction (150+ slots) +- Policy layer fully X11-free (zero direct Xlib calls in WM core) +- wlroots compositor (headless + nested modes) +- XDG shell + XWayland bridge +- Frame rendering (titlebar, buttons, resizebar) via pixman → wlr_scene_buffer +- Menu rendering + highlighting + item activation +- Root menu from config file +- Dock rendering (SHM panel, icon slots, app tracking) +- Clip rendering +- Background rendering +- Mouse input (motion, buttons, scroll, grabs) +- Keyboard input (XKB, modifiers, keysym translation) +- Interactive move/resize with edge detection + cursor changes +- Click-to-focus via hit-testing +- Run dialog (WINGs modal loop with compositor tick) +- Window manage/unmanage lifecycle (XDG toplevel + XWayland) +- Focus delivery (wlr_seat_keyboard_notify_enter) +- WINGs Wayland backend (views, fonts/PangoFT2, events/ring buffer, rendering/pixman, colors) +- Smoke test suite (30+ tests in tests/wl-smoke-test.sh) + +--- + +## Phase A: Core Window Management Gaps + +Priority: HIGH — needed for daily-driver use. + +| # | Task | Key Files | Complexity | +|---|------|-----------|------------| +| A1 | Stacking (raise/lower via scene graph) | stacking.c, wlr_scene_node_raise/lower | Low | +| A2 | Window maximize (full/half/vertical/horizontal) | actions.c, usable area calc | Low-Medium | +| A3 | Window shade (titlebar collapse) | actions.c, frame buffer resize | Low | +| A4 | Workspace switching (create/destroy/switch) | workspace.c, scene tree reparenting | Medium | +| A5 | Alt-Tab window cycling + switch panel | cycling.c, switchpanel.c, vtable switchpanel_enter | Medium | +| A6 | Window iconification (minimize) + miniwindow | icon.c, appicon.c, vtable icon_paint/icon_update_pixmap | Medium | +| A7 | Window placement algorithms (smart/cascade/random) | placement.c (may already work via vtable) | Low | + +--- + +## Phase B: Dock & App Icons + +| # | Task | Key Files | Complexity | +|---|------|-----------|------------| +| B1 | App icon rendering (load .tiff/.png, render into dock) | appicon.c, wrlib loaders, vtable appicon_show | Medium | +| B2 | Dock icon drag & drop (reorder, add/remove) | dock.c DnD handlers | Medium | +| B3 | Dock auto-launch on login | dock.c session restore | Low | +| B4 | Clip workspace indicator (number, arrows) | dock.c clip paint | Low | +| B5 | Drawer support (horizontal dock extension) | dock.c drawer subsystem | Medium | +| B6 | Icon bounce on launch | vtable appicon_bounce | Low | + +--- + +## Phase C: Visual Polish & Animations + +| # | Task | Key Files | Complexity | +|---|------|-----------|------------| +| C1 | Slide animations (iconify/deiconify zoom) | vtable slide_window | Medium | +| C2 | Menu scrolling (long menus) | menu.c scroll logic | Low | +| C3 | Balloon tooltips (dock icon hover) | vtable balloon_show | Low | +| C4 | Cursor themes (xcursor loading) | wlr_xcursor_manager | Low | +| C5 | Window opacity (NETWM alpha) | vtable netwm_set_opacity | Low | + +--- + +## Phase D: WINGs Widgets (for Preferences & Inspectors) + +Core rendering backends exist. Complex widgets need verification/porting: + +| Widget | Needed For | Notes | +|--------|-----------|-------| +| WMList | File panels, icon chooser | Test | +| WMBrowser | File browser in dialogs | Test | +| WMColorPanel | Color picker in prefs | May need porting | +| WMFontPanel | Font selection | May need porting | +| WMFilePanel | Open/save dialogs | Test | +| WMTabView | Preferences app tabs | Test | +| WMScroller/WMScrollView | Scrollable content | Test | +| WMSlider | Preference sliders | Test | +| WMPopUpButton | Dropdown menus in prefs | Test | +| WMTextField (advanced) | Multi-line editing | Test | +| Drag & Drop | Icon DnD, file DnD | Likely needs porting | + +--- + +## Phase E: Dialogs & Panels + +| # | Task | Key Files | Complexity | +|---|------|-----------|------------| +| E1 | Window Inspector (per-window attributes) | winspector.c | High (many widgets) | +| E2 | Info Panel (about dialog) | dialog.c | Low | +| E3 | Crash Dialog (recovery UI) | dialog.c | Low | +| E4 | Icon Chooser (browse icon files) | dialog.c | Medium | +| E5 | Preferences App (full GUI) | WPrefs.app/ | High | + +--- + +## Phase F: wrlib Wayland Integration + +| # | Task | Description | Complexity | +|---|------|-------------|------------| +| F1 | RContext for Wayland | pixman-backed RContext without X11 Display | Medium | +| F2 | RImage → pixman_image conversion | Direct path without XImage | Low (may exist) | +| F3 | Texture rendering verification | Solid, gradient, pixmap textures | Verify | +| F4 | Image format loaders | PNG/JPEG/GIF/WebP (X11-independent) | Verify | + +--- + +## Phase G: Session & Persistence + +| # | Task | Key Files | Complexity | +|---|------|-----------|------------| +| G1 | Session save/restore (positions, workspaces) | session.c | Medium | +| G2 | Dock state persistence (icon layout) | dock.c state files | Low (may work) | +| G3 | Defaults database (~/GNUstep/Defaults/) | defaults.c | Verify | +| G4 | Autostart (launch apps from session) | startup.c | Low | + +--- + +## Phase H: Advanced Compositor Features + +| # | Task | Description | Complexity | +|---|------|-------------|------------| +| H1 | Multi-monitor (output layout, per-output workspaces) | wlr_output_layout, screen.c | High | +| H2 | HiDPI / output scaling | wlr_output_set_scale, buffer scaling | Medium | +| H3 | Gamma control (night light) | Already advertised, wire up | Low | +| H4 | Screen lock integration | ext-session-lock-v1 protocol | Medium | +| H5 | Clipboard (copy/paste between clients) | wl_data_device (may work via wlroots) | Verify | +| H6 | Primary selection | Already advertised | Verify | +| H7 | Screencopy (screenshots) | Already advertised | Verify | +| H8 | DMA-BUF (GPU buffer sharing) | Already advertised | Verify | +| H9 | Input method (IME support) | Already advertised | Medium | + +--- + +## Phase I: EWMH / Foreign Toplevel + +| # | Task | Description | Complexity | +|---|------|-------------|------------| +| I1 | Foreign toplevel list (taskbar integration) | Already advertised, verify events | Low | +| I2 | XDG activation (focus stealing prevention) | Wire to policy | Low | +| I3 | Idle inhibit (prevent blank during video) | Already advertised | Low | + +--- + +## Phase J: DRM/KMS Backend (Native) + +For running without a host compositor: + +| # | Task | Description | Complexity | +|---|------|-------------|------------| +| J1 | DRM backend | wlr_backend_autocreate | Medium | +| J2 | libinput (real hardware input) | wlroots handles this | Low | +| J3 | VT switching | wlr_session | Low | +| J4 | Multi-GPU | Secondary GPU rendering | High | + +--- + +## Suggested Implementation Order + +``` +NOW (daily-driver basics): + A1. Stacking (raise/lower) + A2. Maximize + A3. Shade + A4. Workspace switching + A5. Alt-Tab cycling + A6. Icon/minimize + A7. Placement (verify) + +NEXT (visual completeness): + B1. App icon rendering (wrlib → pixman) + F1. wrlib RContext for Wayland + C1. Slide animations + C3. Balloon tooltips + +THEN (full feature parity): + D*. Test all WINGs widgets + E1. Window Inspector + E4. Icon Chooser + G*. Session persistence + B2-B6. Full dock features + +LATER (production-ready): + H1. Multi-monitor + H2. HiDPI + J*. DRM/KMS native backend + E5. WPrefs app +``` + +--- + +## Technical Notes + +1. **Rendering pattern**: pixman_image_t → wlr_scene_buffer → position in scene graph +2. **wrlib is mostly X11-independent** — only context.c and convert.c touch X11 +3. **WINGs complex widgets** are the hardest remaining work (subtle X11 assumptions in events/coords) +4. **wlroots handles compositor plumbing** — protocols advertised, work is wiring to policy +5. **wl_backend.c (316K) should be split** into wl_frame.c, wl_menu.c, wl_input.c, wl_toplevel.c, wl_output.c as complexity grows +6. **Many vtable slots are no-ops** — stubs exist, need implementation following the pixman→scene pattern + +--- + +## Key Source Files Reference + +| File | Size | Purpose | +|------|------|---------| +| src/backend/backend.h | 74K | Vtable definition | +| src/backend/backend_types.h | 16K | Backend-neutral types | +| src/backend/wayland/wl_backend.c | 316K | Wayland compositor | +| src/backend/x11/x11_backend.c | 150K | X11 backend | +| src/window.c | 83K | Window management | +| src/dock.c | 130K | Dock/Clip/Drawers | +| src/menu.c | 61K | Menu system | +| src/actions.c | 68K | Window actions | +| src/defaults.c | 102K | Preferences/config | +| src/event.c | 61K | Event dispatch | +| src/moveres.c | 67K | Move/resize | +| src/wmspec.c | 61K | EWMH compliance | +| src/main.c | 22K | Entry point | +| WINGs/WINGs.h | 56K | Toolkit public API | +| wrlib/wraster.h | 17K | Raster lib public API | diff --git a/src/backend/wayland/wl_backend.c b/src/backend/wayland/wl_backend.c index 1ac2e5c..c7db311 100644 --- a/src/backend/wayland/wl_backend.c +++ b/src/backend/wayland/wl_backend.c @@ -119,6 +119,7 @@ #include #include #include +#include #include #include #include @@ -241,6 +242,7 @@ struct wl_toplevel_view { struct wl_listener request_fullscreen_listener;/* Phase 16 */ struct wl_listener request_minimize_listener; /* Phase 16 */ struct wl_listener set_app_id_listener; /* Phase 16 */ + struct wl_listener commit_listener; /* initial configure */ /* Phase 17: XWayland — set for X11 windows, NULL for XDG toplevels. */ struct wlr_xwayland_surface *xw_surface; /* Path 1: geometry listener — keeps scene node in sync when X11 window moves. */ @@ -1091,6 +1093,7 @@ handle_toplevel_destroy(struct wl_listener *listener, void *data) wl_list_remove(&view->request_fullscreen_listener.link); wl_list_remove(&view->request_minimize_listener.link); wl_list_remove(&view->set_app_id_listener.link); + wl_list_remove(&view->commit_listener.link); wl_list_remove(&view->link); /* Phase 18: remove scene node. */ if (view->scene_node) { @@ -1110,17 +1113,47 @@ handle_toplevel_destroy(struct wl_listener *listener, void *data) /* * Phase 64: new_toplevel_decoration listener. - * Immediately respond with CLIENT_SIDE so clients draw their own - * title bars and borders (the Wayland path has no X11 frame rendering). + * Respond with CLIENT_SIDE so clients draw their own decorations. + * Must defer until the surface is initialized (wlroots 0.19). */ +struct wl_deferred_decoration { + struct wl_listener commit_listener; + struct wlr_xdg_toplevel_decoration_v1 *deco; +}; + +static void +handle_deferred_decoration_commit(struct wl_listener *listener, void *data) +{ + struct wl_deferred_decoration *dd = + wl_container_of(listener, dd, commit_listener); + struct wlr_xdg_surface *surface = dd->deco->toplevel->base; + (void)data; + + if (!surface->initial_commit) + return; + + wl_list_remove(&dd->commit_listener.link); + wlr_xdg_toplevel_decoration_v1_set_mode(dd->deco, + WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE); + wfree(dd); +} + static void handle_new_toplevel_decoration(struct wl_listener *listener, void *data) { struct wlr_xdg_toplevel_decoration_v1 *deco = data; (void)listener; - wlr_xdg_toplevel_decoration_v1_set_mode(deco, - WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE); - wwarning("wl_backend: decoration mode=client_side for new toplevel"); + + struct wlr_xdg_surface *surface = deco->toplevel->base; + if (surface->initialized) { + wlr_xdg_toplevel_decoration_v1_set_mode(deco, + WLR_XDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE); + } else { + struct wl_deferred_decoration *dd = wmalloc(sizeof(*dd)); + dd->deco = deco; + dd->commit_listener.notify = handle_deferred_decoration_commit; + wl_signal_add(&surface->surface->events.commit, &dd->commit_listener); + } } /* ------------------------------------------------------------------ */ @@ -1523,30 +1556,43 @@ handle_xwayland_new_surface(struct wl_listener *listener, void *data) } /* - * Phase 36 / 38: new XDG surface listener. - * Allocates a wl_toplevel_view, assigns a synthetic WNativeWindow ID, - * wires the three lifecycle listeners, and schedules the initial configure. + * handle_xdg_toplevel_initial_commit -- wlroots 0.19 requires that the + * initial configure is sent in response to the client's first (empty) commit, + * not at new_surface time. The surface is only "initialized" after this + * commit, so schedule_configure is safe here. */ static void -handle_xdg_new_surface(struct wl_listener *listener, void *data) +handle_xdg_toplevel_initial_commit(struct wl_listener *listener, void *data) { - struct wlr_xdg_surface *xdg_surface = data; + struct wl_toplevel_view *view = + wl_container_of(listener, view, commit_listener); + (void)data; + + if (!view->xdg_surface || !view->xdg_surface->initial_commit) + return; + + /* Only need this once — remove ourselves. */ + wl_list_remove(&view->commit_listener.link); + wl_list_init(&view->commit_listener.link); + + wlr_xdg_toplevel_set_size(view->xdg_surface->toplevel, 0, 0); + wlr_xdg_surface_schedule_configure(view->xdg_surface); +} + +/* + * Phase 36 / 38: new XDG toplevel listener (wlroots 0.19+). + * Fires when the client calls get_toplevel on an xdg_surface. + * Allocates a wl_toplevel_view, assigns a synthetic WNativeWindow ID, + * wires lifecycle listeners, and sets up the initial commit handler. + */ +static void +handle_xdg_new_toplevel(struct wl_listener *listener, void *data) +{ + struct wlr_xdg_toplevel *toplevel = data; + struct wlr_xdg_surface *xdg_surface = toplevel->base; struct wl_toplevel_view *view; (void)listener; - if (xdg_surface->role == WLR_XDG_SURFACE_ROLE_POPUP) { - /* Phase 66: schedule the initial configure for XDG popups so clients - * that use popups (context menus, autocomplete, tooltips) can commit - * their first buffer without stalling. We do not manage popups in - * the policy layer — they are self-contained transient surfaces. */ - wlr_xdg_surface_schedule_configure(xdg_surface); - wwarning("wl_backend: new XDG popup (scheduled configure)"); - return; - } - - if (xdg_surface->role != WLR_XDG_SURFACE_ROLE_TOPLEVEL) - return; - view = wmalloc(sizeof(*view)); memset(view, 0, sizeof(*view)); view->xdg_surface = xdg_surface; @@ -1556,34 +1602,36 @@ handle_xdg_new_surface(struct wl_listener *listener, void *data) view->unmap_listener.notify = handle_toplevel_unmap; view->destroy_listener.notify = handle_toplevel_destroy; view->title_listener.notify = handle_toplevel_title; - view->request_move_listener.notify = handle_toplevel_request_move; /* Phase 15 */ - view->request_resize_listener.notify = handle_toplevel_request_resize; /* Phase 15 */ - view->request_maximize_listener.notify = handle_toplevel_request_maximize; /* Phase 16 */ - view->request_fullscreen_listener.notify = handle_toplevel_request_fullscreen; /* Phase 16 */ - view->request_minimize_listener.notify = handle_toplevel_request_minimize; /* Phase 16 */ - view->set_app_id_listener.notify = handle_toplevel_set_app_id; /* Phase 16 */ + view->request_move_listener.notify = handle_toplevel_request_move; + view->request_resize_listener.notify = handle_toplevel_request_resize; + view->request_maximize_listener.notify = handle_toplevel_request_maximize; + view->request_fullscreen_listener.notify = handle_toplevel_request_fullscreen; + view->request_minimize_listener.notify = handle_toplevel_request_minimize; + view->set_app_id_listener.notify = handle_toplevel_set_app_id; wl_signal_add(&xdg_surface->surface->events.map, &view->map_listener); wl_signal_add(&xdg_surface->surface->events.unmap, &view->unmap_listener); - wl_signal_add(&xdg_surface->events.destroy, &view->destroy_listener); - wl_signal_add(&xdg_surface->toplevel->events.set_title, &view->title_listener); - wl_signal_add(&xdg_surface->toplevel->events.request_move, &view->request_move_listener); - wl_signal_add(&xdg_surface->toplevel->events.request_resize, &view->request_resize_listener); - wl_signal_add(&xdg_surface->toplevel->events.request_maximize, &view->request_maximize_listener); - wl_signal_add(&xdg_surface->toplevel->events.request_fullscreen, &view->request_fullscreen_listener); - wl_signal_add(&xdg_surface->toplevel->events.request_minimize, &view->request_minimize_listener); - wl_signal_add(&xdg_surface->toplevel->events.set_app_id, &view->set_app_id_listener); + wl_signal_add(&toplevel->events.destroy, &view->destroy_listener); + wl_signal_add(&toplevel->events.set_title, &view->title_listener); + wl_signal_add(&toplevel->events.request_move, &view->request_move_listener); + wl_signal_add(&toplevel->events.request_resize, &view->request_resize_listener); + wl_signal_add(&toplevel->events.request_maximize, &view->request_maximize_listener); + wl_signal_add(&toplevel->events.request_fullscreen, &view->request_fullscreen_listener); + wl_signal_add(&toplevel->events.request_minimize, &view->request_minimize_listener); + wl_signal_add(&toplevel->events.set_app_id, &view->set_app_id_listener); + + /* wlroots 0.19: send initial configure on the client's first commit, + * when the surface becomes initialized. */ + view->commit_listener.notify = handle_xdg_toplevel_initial_commit; + wl_signal_add(&xdg_surface->surface->events.commit, &view->commit_listener); wl_list_insert(&wl_state.toplevel_list, &view->link); wl_state.managed_surface_count++; wwarning("wl_backend: new XDG toplevel id=0x%lx app_id=%s title=%s", view->id, - xdg_surface->toplevel->app_id ? xdg_surface->toplevel->app_id : "(none)", - xdg_surface->toplevel->title ? xdg_surface->toplevel->title : "(none)"); - - /* Phase 37: initial configure so the client can commit its first buffer. */ - wlr_xdg_surface_schedule_configure(xdg_surface); + toplevel->app_id ? toplevel->app_id : "(none)", + toplevel->title ? toplevel->title : "(none)"); } /* ------------------------------------------------------------------ */ @@ -2157,6 +2205,7 @@ wl_display_open(const char *name) /* Phase 35: advertise core Wayland protocol globals. */ wl_state.compositor = wlr_compositor_create(wl_state.display, 6, wl_state.renderer); + wlr_subcompositor_create(wl_state.display); wl_state.seat = wlr_seat_create(wl_state.display, "seat0"); wl_state.data_device_mgr = wlr_data_device_manager_create(wl_state.display); @@ -2432,9 +2481,9 @@ wl_display_open(const char *name) wl_state.next_synthetic_id = 0x10000000UL; wl_state.managed_surface_count = 0; - /* Phase 36: listen for new XDG toplevel surfaces. */ - wl_state.xdg_new_surface_listener.notify = handle_xdg_new_surface; - wl_signal_add(&wl_state.xdg_shell->events.new_surface, + /* Phase 36: listen for new XDG toplevels (wlroots 0.19+). */ + wl_state.xdg_new_surface_listener.notify = handle_xdg_new_toplevel; + wl_signal_add(&wl_state.xdg_shell->events.new_toplevel, &wl_state.xdg_new_surface_listener); /* Path 1: XWayland bridge — start Xwayland eagerly (lazy=false) so that @@ -2949,6 +2998,10 @@ wl_event_loop_run(void) wwarning("wl_backend: X11 event source registered for WINGs dispatch"); } + wwarning("wl_backend: DISPLAY=%s WAYLAND_DISPLAY=%s", + getenv("DISPLAY") ? getenv("DISPLAY") : "(unset)", + getenv("WAYLAND_DISPLAY") ? getenv("WAYLAND_DISPLAY") : "(unset)"); + /* Phase A: register compositor dispatch callback so WINGs modal loops * (WMRunModalLoop, color panels, etc.) can tick the event loop. */ {