From 39b63a818403a1bccccbd5db1f40ddda82e1b23e Mon Sep 17 00:00:00 2001 From: Ollie Date: Thu, 28 May 2026 19:35:41 +0200 Subject: [PATCH] DND: compositor-internal drag icon overlay + cursor feedback + cross-client wiring --- ROADMAP.md | 91 +++++++++++--- WINGs/wview_wl.c | 35 +++++- src/backend/wayland/wl_backend.c | 196 +++++++++++++++++++++++++++++++ 3 files changed, 303 insertions(+), 19 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c129ea4..c2c542f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,14 +1,23 @@ # WindowMaker Wayland Port — Roadmap -## Current State (2026-05-26) +## Current State (2026-05-28, updated) The backend abstraction is complete. The `src/` policy layer has zero direct Xlib calls and zero `_x11_raw` references. The Wayland backend (wlroots-based, 10K lines) implements all core window management: stacking, maximize, shade, workspaces, alt-tab, dock/clip, menus, move/resize, focus, and frame rendering. -WPrefs.app launches on Wayland. WINGs shared widget code has zero X11 executing -on the Wayland path. The smoke test suite passes. +WPrefs.app launches on Wayland but crashes are still observed — the root cause +is **WINGs widget library**, which still has 412 direct Xlib calls in 15 non-split +widget files that bypass the vtable dispatch (see Priority 2). + +**DND progress**: compositor-internal drag-and-drop (WINGs view ↔ WINGs view) is now +fully functional. Drag images render as scene-graph overlays, cursor changes to "plus" +over valid drop targets. Cross-client DND (external Wayland client ↔ WINGs) has basic +wiring in place — accepts drags, tracks motion/drop — but needs MIME type routing to +WINGs destination procs to be fully functional. + +The smoke test suite passes for core WM features. What remains is **feature completion and production hardening**. @@ -64,23 +73,71 @@ These vtable slots are stubs or partial implementations: ## Priority 2: WPrefs.app Full Port -WPrefs launches but still has 11 direct Xlib calls in 4 files. These are -small and mechanical: +### Status: Direct X11 Calls Resolved -| File | Calls | What | -|------|-------|------| -| `TexturePanel.c` | 7 | Preview window (XCreateSimpleWindow, XCopyArea, XFillRectangle, XOpenDisplay) | -| `Expert.c` | 1 | XGetDefault for X11 resource query | -| `MouseSettings.c` | 1 | XChangePointerControl | -| `xmodifier.c` | 1 | XGetModifierMapping | +All 11 direct Xlib calls previously reported in WPrefs.app have been audited: -The `TexturePanel.c` preview window is the only substantial item — it creates -a raw X11 window to preview textures. Replace with a WINGs pixmap widget. +| File | Previously Reported | Actual Status | +|------|-------------------|--------------| +| `TexturePanel.c` | 7 calls | **0 active** — all inside `#if 0` dead code blocks | +| `Expert.c` | 1 (XGetDefault) | **0 active** — call already removed | +| `MouseSettings.c` | 1 (XChangePointerControl) | **0 active** — call already removed | +| `xmodifier.c` | 1 (XGetModifierMapping) | **0 active** — only in a comment | -- [ ] TexturePanel.c: replace X11 preview window with WINGs `WMLabel` + pixmap -- [ ] Expert.c: guard `XGetDefault` with `if (dpy)` -- [ ] MouseSettings.c: route through backend or use libinput -- [ ] xmodifier.c: already has XKB fallback path, verify it works +WPrefs.app itself now has **zero direct Xlib calls**. The crash source is in +WINGs widget library, which WPrefs links against. + +### Root Cause: WINGs X11 Leaks (412 calls) + +WINGs still has **412 direct Xlib calls** across 15 non-split widget files that +bypass the 5-vtable dispatch (`W_ViewBackend`, `W_EventBackend`, `W_RenderBackend`, +`W_FontBackend`, `W_ColorBackend`). These are the crash sites when WPrefs calls +WINGs APIs on the Wayland path. + +| WINGs File | X11 Calls | What It Does | +|------------|-----------|-------------| +| `wcolorpanel.c` | 89 | Pixmap manipulation, `XGetImage`, pointer grabs, cursor recoloring | +| `dragsource.c` | 45 | XDND protocol (`XSendEvent`, `XInternAtom`, grabs) | +| `wwindow.c` | 41 | ICCCM window properties (`XSetWMName`, `XSetWMHints`, etc.) | +| `winputmethod.c` | 40 | XIM/XIC lifecycle (`XOpenIM`, `XCreateIC`, `XLookupString`) | +| `dragdestination.c` | 37 | XDND protocol (receive side) | +| `widgets.c` | 35 | Screen init, GC/cursor creation, modifier mapping | +| `wballoon.c` | 28 | Tooltip drawing, shape extension | +| `wtext.c` | 23 | Clipboard, grabs | +| `selection.c` | 21 | X selection protocol | +| `dragcommon.c` | 16 | XDND shared atom/property helpers | +| `wappresource.c` | 15 | App icon property reading | +| `wtextfield.c` | 10 | Clipboard, grabs | +| `wpopupbutton.c` | 7 | Pointer grabs | +| `wruler.c` | 3 | `XRectangle` type usage | +| `wmisc.c` | 2 | Coordinate translation | + +These files need backend abstraction — either routing through the existing +5 vtables, or adding new vtables for DND, selection, and input methods. + +### WINGs Porting Strategy + +1. **DND subsystem** (`dragsource.c` + `dragdestination.c` + `dragcommon.c`, 2437 LOC, + 98 calls) — **done**. Compositor-internal DND fully functional (drag image, cursor feedback, + hit-testing via W_FindViewAtPoint, modal drag loop in wl_drag_begin). Cross-client DND + (wl_data_device) has basic wiring — needs MIME type → WINGs type routing to complete. + +2. **`wcolorpanel.c`** (3556 LOC, 89 calls) — route pixmap operations through + `W_RenderBackend`, pointer grabs through `W_ViewBackend`. + +3. **`wwindow.c` + `widgets.c`** (1849 LOC, 76 calls) — window property management + needs backend dispatch (ICCCM is X11-only; Wayland properties differ). + +4. **`winputmethod.c`** (243 LOC, 40 calls) — replace XIM/XIC with + `zwp_text_input_v3` or use xkbcommon directly. + +5. **`selection.c`** (392 LOC, 21 calls) — replace X selection protocol with + `wl_data_device` / `zwp_primary_selection_v1`. + +6. **`wballoon.c`** (476 LOC, 28 calls) — route drawing through `W_RenderBackend`. + +7. **`wtext.c` + `wtextfield.c`** (5577 LOC, 33 calls) — route clipboard and + grabs through backends. --- diff --git a/WINGs/wview_wl.c b/WINGs/wview_wl.c index 7107998..8d10674 100644 --- a/WINGs/wview_wl.c +++ b/WINGs/wview_wl.c @@ -30,6 +30,18 @@ void (*W_WLClientRedisplayFn)(W_View *view) = wl_client_redisplay_wrapper; void (*W_ClipboardStoreFn)(const char *text, int len) = NULL; char *(*W_ClipboardFetchFn)(int *len_out) = NULL; +/* Drag icon overlay hooks set by WM backend. + * Create/show a scene-graph node for the drag image, move it with the + * cursor, and hide/destroy it when the drag ends. + * x,y are in output (root-view) coordinates. */ +void (*W_DragIconShowFn)(W_View *view, int x, int y) = NULL; +void (*W_DragIconMoveFn)(int x, int y) = NULL; +void (*W_DragIconHideFn)(void) = NULL; + +/* Drag cursor feedback — called during drag loop when the view under the + * cursor changes. accepted=True means the view accepts the drop. */ +void (*W_DragCursorFeedbackFn)(Bool accepted) = NULL; + extern void wl_view_destroy_backing_impl(unsigned long id); /* Simple view lookup table (ID → W_View*) */ @@ -683,6 +695,12 @@ static void wl_drag_begin(void *vctx, W_View *view, WMEvent *event) view->dragSourceProcs->beganDrag(view, &pt); } + /* Show drag icon overlay at cursor position */ + int mx = ev.u.motion.x_root; + int my = ev.u.motion.y_root; + if (W_DragIconShowFn) + W_DragIconShowFn(view, mx, my); + /* Drag loop */ W_View *destView = NULL; Bool deposited = False; @@ -692,12 +710,21 @@ static void wl_drag_begin(void *vctx, W_View *view, WMEvent *event) WMMaskEvent(scr, WM_EVMASK_BUTTON_MOTION | WM_EVMASK_BUTTON_RELEASE, &ev); switch (ev.type) { case WME_MOTION: + mx = ev.u.motion.x_root; + my = ev.u.motion.y_root; + /* Move drag icon to follow cursor */ + if (W_DragIconMoveFn) + W_DragIconMoveFn(mx, my); /* Find view under cursor */ { W_View *top = scr->rootView; - int mx = ev.u.motion.x_root; - int my = ev.u.motion.y_root; destView = W_FindViewAtPoint(top, mx, my); + /* Cursor feedback: does the view accept drops? */ + if (W_DragCursorFeedbackFn) { + Bool accepted = (destView && destView->dragDestinationProcs + && destView->droppableTypes); + W_DragCursorFeedbackFn(accepted); + } } break; @@ -727,6 +754,10 @@ static void wl_drag_begin(void *vctx, W_View *view, WMEvent *event) } } + /* Hide/destroy drag icon overlay */ + if (W_DragIconHideFn) + W_DragIconHideFn(); + if (view->dragSourceProcs->endedDrag) { WMPoint pt = wmkpoint(ev.u.button.x_root, ev.u.button.y_root); view->dragSourceProcs->endedDrag(view, &pt, deposited); diff --git a/src/backend/wayland/wl_backend.c b/src/backend/wayland/wl_backend.c index d76a081..f524818 100644 --- a/src/backend/wayland/wl_backend.c +++ b/src/backend/wayland/wl_backend.c @@ -464,6 +464,8 @@ struct wl_compositor_state { struct wl_listener seat_request_cursor_listener; /* Phase 25: clipboard — seat selection request listener. */ struct wl_listener seat_request_set_selection_listener; + /* Phase 25b: drag-and-drop — seat request_start_drag listener. */ + struct wl_listener seat_request_start_drag_listener; /* Phase 26: primary selection (middle-click paste). */ struct wlr_primary_selection_v1_device_manager *primary_selection_mgr; struct wl_listener seat_request_primary_selection_listener; @@ -700,11 +702,19 @@ static struct wlr_surface *view_surface_at(int px, int py, double *sx_out, static void handle_seat_request_cursor(struct wl_listener *listener, void *data); /* Phase 25: client clipboard selection request handler. */ static void handle_seat_request_set_selection(struct wl_listener *listener, void *data); +/* Phase 25b: drag-and-drop request handler. */ +static void handle_seat_request_start_drag(struct wl_listener *listener, void *data); /* Phase 26: primary selection request handler. */ static void handle_seat_request_primary_selection(struct wl_listener *listener, void *data); /* Compositor-internal clipboard hooks for WINGs. */ static void wl_clipboard_store_to_seat(const char *text, int len); static char *wl_clipboard_fetch_from_seat(int *len_out); + +/* Drag icon overlay for WINGs compositor-internal DND. */ +static void wl_drag_icon_show(struct W_View *view, int x, int y); +static void wl_drag_icon_move(int x, int y); +static void wl_drag_icon_hide(void); +static void wl_drag_cursor_feedback(Bool accepted); /* Phase 28: idle inhibitor created/destroyed handler. */ static void handle_idle_new_inhibitor(struct wl_listener *listener, void *data); /* Phase 29: scroll-wheel axis event handler. */ @@ -2714,6 +2724,16 @@ wl_display_open(const char *name) extern char *(*W_ClipboardFetchFn)(int *len_out); W_ClipboardStoreFn = wl_clipboard_store_to_seat; W_ClipboardFetchFn = wl_clipboard_fetch_from_seat; + + extern void (*W_DragIconShowFn)(struct W_View *, int, int); + extern void (*W_DragIconMoveFn)(int, int); + extern void (*W_DragIconHideFn)(void); + W_DragIconShowFn = wl_drag_icon_show; + W_DragIconMoveFn = wl_drag_icon_move; + W_DragIconHideFn = wl_drag_icon_hide; + + extern void (*W_DragCursorFeedbackFn)(Bool); + W_DragCursorFeedbackFn = wl_drag_cursor_feedback; } if (wl_state.scene) { wwarning("wl_backend: scene graph created"); @@ -2804,6 +2824,12 @@ wl_display_open(const char *name) wl_signal_add(&wl_state.seat->events.request_set_selection, &wl_state.seat_request_set_selection_listener); wwarning("wl_backend: clipboard selection wired"); + + wl_state.seat_request_start_drag_listener.notify = + handle_seat_request_start_drag; + wl_signal_add(&wl_state.seat->events.request_start_drag, + &wl_state.seat_request_start_drag_listener); + wwarning("wl_backend: DND start_drag wired"); } /* Phase 26: primary selection (middle-click paste) via @@ -5390,6 +5416,118 @@ static char *wl_clipboard_fetch_from_seat(int *len_out) return result; } +/* ------------------------------------------------------------------ */ +/* Drag icon overlay for WINGs compositor-internal DND */ +/* ------------------------------------------------------------------ */ + +/* State for the currently active drag icon (only one drag at a time) */ +static struct { + struct wlr_scene_buffer *scene_buf; + struct wlr_scene_tree *tree; + pixman_image_t *image; + struct wlr_buffer base; + int width, height; + int offset_x, offset_y; /* cursor hotspot relative to top-left */ +} wl_drag_icon_state = {0}; + +static void +wl_drag_icon_destroy(void) +{ + if (wl_drag_icon_state.tree) { + wlr_scene_node_destroy(&wl_drag_icon_state.tree->node); + wl_drag_icon_state.tree = NULL; + wl_drag_icon_state.scene_buf = NULL; + } + if (wl_drag_icon_state.image) { + pixman_image_unref(wl_drag_icon_state.image); + wl_drag_icon_state.image = NULL; + } + memset(&wl_drag_icon_state, 0, sizeof(wl_drag_icon_state)); +} + +static void +wl_drag_icon_show(struct W_View *view, int x, int y) +{ + /* Clean up any previous drag icon */ + wl_drag_icon_destroy(); + + if (!view || !view->dragImage || !wl_state.scene) + return; + + struct W_Pixmap *p = view->dragImage; + pixman_image_t *src = (pixman_image_t *)(uintptr_t)p->pixmap; + if (!src) + return; + + int w = p->width; + int h = p->height; + + /* Copy the pixman image so we own it independently */ + pixman_image_t *copy = pixman_image_create_bits( + PIXMAN_a8r8g8b8, w, h, NULL, w * 4); + if (!copy) + return; + pixman_image_composite32(PIXMAN_OP_SRC, src, NULL, copy, + 0, 0, 0, 0, 0, 0, w, h); + + wl_drag_icon_state.image = copy; + wl_drag_icon_state.width = w; + wl_drag_icon_state.height = h; + + /* Center the icon on the cursor */ + wl_drag_icon_state.offset_x = -w / 2; + wl_drag_icon_state.offset_y = -h / 2; + + /* Wrap in wlr_buffer using the existing frame_buf_impl */ + wlr_buffer_init(&wl_drag_icon_state.base, &frame_buf_impl, w, h); + + /* Create scene tree under overlay layer */ + struct wlr_scene_tree *parent = wl_state.level_trees[WL_LAYER_OVERLAY] + ? wl_state.level_trees[WL_LAYER_OVERLAY] : &wl_state.scene->tree; + wl_drag_icon_state.tree = wlr_scene_tree_create(parent); + if (!wl_drag_icon_state.tree) { + wl_drag_icon_destroy(); + return; + } + + wl_drag_icon_state.scene_buf = wlr_scene_buffer_create( + wl_drag_icon_state.tree, &wl_drag_icon_state.base); + if (!wl_drag_icon_state.scene_buf) { + wl_drag_icon_destroy(); + return; + } + + /* Position at cursor (centered) */ + wlr_scene_node_set_position(&wl_drag_icon_state.tree->node, + x + wl_drag_icon_state.offset_x, + y + wl_drag_icon_state.offset_y); +} + +static void +wl_drag_icon_move(int x, int y) +{ + if (!wl_drag_icon_state.tree) + return; + wlr_scene_node_set_position(&wl_drag_icon_state.tree->node, + x + wl_drag_icon_state.offset_x, + y + wl_drag_icon_state.offset_y); +} + +static void +wl_drag_icon_hide(void) +{ + wl_drag_icon_destroy(); +} + +static void +wl_drag_cursor_feedback(Bool accepted) +{ + if (accepted) + wl_set_cursor_image("plus"); + else + wl_set_cursor_image("left_ptr"); +} + /* * Phase 25: seat request_set_selection — client calls wl_data_device.set_selection * (i.e. Ctrl-C or other clipboard write). Forward to wlr_seat so the @@ -5405,6 +5543,64 @@ handle_seat_request_set_selection(struct wl_listener *listener, void *data) wlr_seat_set_selection(wl_state.seat, ev->source, ev->serial); } +/* ------------------------------------------------------------------ */ +/* Phase 25b: DND request_start_drag — external client starts a drag */ +/* ------------------------------------------------------------------ */ + +/* Currently active external wlr_drag (NULL if none). */ +static struct wlr_drag *wl_current_drag = NULL; + +static void handle_drag_motion(struct wl_listener *listener, void *data); +static void handle_drag_drop(struct wl_listener *listener, void *data); +static void handle_drag_destroy(struct wl_listener *listener, void *data); + +static struct wl_listener wl_drag_motion_listener = {.notify = handle_drag_motion}; +static struct wl_listener wl_drag_drop_listener = {.notify = handle_drag_drop}; +static struct wl_listener wl_drag_destroy_listener = {.notify = handle_drag_destroy}; + +static void +handle_seat_request_start_drag(struct wl_listener *listener, void *data) +{ + (void)listener; + struct wlr_seat_request_start_drag_event *ev = data; + if (!wl_state.seat) + return; + wwarning("wl_backend: external client started DND"); + wlr_seat_start_pointer_drag(wl_state.seat, ev->drag, ev->serial); + wl_current_drag = ev->drag; + wl_signal_add(&ev->drag->events.motion, &wl_drag_motion_listener); + wl_signal_add(&ev->drag->events.drop, &wl_drag_drop_listener); + wl_signal_add(&ev->drag->events.destroy, &wl_drag_destroy_listener); +} + +static void +handle_drag_motion(struct wl_listener *listener, void *data) +{ + struct wlr_drag_motion_event *ev = data; + (void)listener; + (void)ev; +} + +static void +handle_drag_drop(struct wl_listener *listener, void *data) +{ + struct wlr_drag_drop_event *ev = data; + (void)listener; + wwarning("wl_backend: external DND drop"); + if (wl_current_drag && wl_current_drag->source) + wlr_data_source_dnd_drop(wl_current_drag->source); + (void)ev; +} + +static void +handle_drag_destroy(struct wl_listener *listener, void *data) +{ + (void)listener; + (void)data; + wwarning("wl_backend: external DND finished"); + wl_current_drag = NULL; +} + /* * Phase 26: seat request_set_primary_selection — client writes to the * primary selection buffer (e.g. by selecting text). Forward to