DND: compositor-internal drag icon overlay + cursor feedback + cross-client wiring
This commit is contained in:
parent
e1eaf17925
commit
39b63a8184
91
ROADMAP.md
91
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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue