DND: compositor-internal drag icon overlay + cursor feedback + cross-client wiring

This commit is contained in:
Ollie 2026-05-28 19:35:41 +02:00
parent e1eaf17925
commit 39b63a8184
3 changed files with 303 additions and 19 deletions

View File

@ -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.
---

View File

@ -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);

View File

@ -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