Add native Wayland file dragging for SDL2 and SDL3

- Load Wayland APIs dynamically and track genuine pointer press serials.
- Serve file URI lists on a dedicated queue and transfer thread.
- Render native drag icons with Copy, rejected and pending feedback.
- Use memfd_create on Linux with a portable temporary-file fallback.
- Suppress synthetic gesture releases and clean up on cancellation,
  window destruction and process exit.
- Add isolated Weston integration tests and document API requirements.
This commit is contained in:
Martín Lucas Golini
2026-10-15 10:58:58 -03:00
parent ad69ee977e
commit ee8f560bde
25 changed files with 1863 additions and 28 deletions
+31 -9
View File
@@ -138,8 +138,8 @@ image just beyond the pointer so the target remains visible.
Images must have RGB or RGBA pixels, be at most 2048 pixels per side, and contain no more than
1,048,576 pixels in total. Each hotspot coordinate must be between -2048 and 2048. On Windows and
X11, alpha is composited onto the preview background; macOS keeps it transparent. The image is
copied during `startDrag()` and may be released or reused as soon as the call returns.
X11, alpha is composited onto the preview background; Wayland and macOS keep it transparent. The
image is copied during `startDrag()` and may be released or reused as soon as the call returns.
On macOS, the default preview is each file's Finder icon, and AppKit draws the item count and the
copy cursor. Preview pixels map to display pixels, so on a Retina display the image appears at half
@@ -186,17 +186,29 @@ Query capabilities for the actual window rather than assuming support from the o
| `dataSource` | Arbitrary native data sources; currently false. |
| `negotiatedTarget` | Native target action/MIME negotiation; currently false. |
Outbound local-file dragging is implemented for Windows, macOS and X11 on Linux/BSD, with SDL2 or
SDL3. Wayland and Haiku currently report no file-source support. On Linux/BSD, X11 headers enable
source support when building eepp; no additional consumer link dependency or build option is needed.
The X11 library is loaded dynamically, and a Wayland window does not load it for dragging.
Outbound local-file dragging is implemented for Windows, macOS, X11 and Wayland, with SDL2 or SDL3.
Haiku currently reports no file-source support. On Linux/BSD, X11 and Wayland client headers enable
their respective transports when building eepp. Wayland requires client headers and a client library
at version 1.20 or newer. Both client libraries load dynamically; no additional consumer link
dependency or build option is needed.
Without the corresponding headers, eepp still builds with that transport disabled; check
`getCapabilities().fileSource` before offering native file dragging.
On Wayland, start the drag from a real left-button press on the source window while that button is
still held. Injecting only eepp/SDL input events cannot authorize a native drag: the compositor must
have issued the press serial. A second button held during the gesture is unsupported. The compositor
moves the drag image, including transparent pixels and its hotspot. A small badge shows Copy,
rejected, or pending target negotiation without changing the supplied image. This requires the core
subcompositor protocol. File reception continues through SDL's ordinary drop events, including drops
into another window of the same application. Compositors must provide version 3 of the core
data-device protocol for Copy negotiation and completion.
On Windows, SDL2 receives files through its legacy file-drop path, which cannot receive the outgoing
OLE gesture used by this source. Use SDL3 for eepp-window-to-eepp-window native drops. Both backends
can drag files out to native OLE targets. On X11, the source window itself is excluded as a native
target; keep same-window operations in your internal UI drag handling. macOS does not exclude it: a
drop onto the source window arrives there as ordinary `FileDropped` events, so keep same-window
operations internal there as well.
target; keep same-window operations in your internal UI drag handling. On macOS and Wayland, a drag
that returns to the source window can deliver ordinary `FileDropped` events there; keep same-window
operations internal on those platforms as well.
Applications that install their own `SDL_SetX11EventHook` must coordinate its use: SDL3 has a single
hook slot, which eepp uses during an active native drag and clears afterwards.
@@ -213,3 +225,13 @@ bin/eepp-native-drag-and-drop /absolute/file.txt /absolute/another-file.txt
Run `bin/eepp-native-dnd-tests` without arguments for a manual playground with temporary sample
files, or supply paths with `--manual`. You control the pointer; drag the card into the receiving
window, a file manager or a browser upload input. Escape cancels the gesture.
For automated Wayland integration checks, build `eepp-native-dnd-tests` and run:
```sh
python3 projects/scripts/test-wayland-dnd.py
```
This requires Weston with its X11 backend, Xvfb, xdotool and ImageMagick. It creates an isolated
Weston session and injects input only there; it does not move your desktop pointer. Protocol logs
are saved in the temporary directory printed by the runner. Normal manual runs never inject input.
+7 -3
View File
@@ -313,11 +313,15 @@ class EE_API Input {
* generating a click and restore capture. The default only resets eepp's input state. */
virtual void resetNativeDragInput();
/** Consume the one delayed native release left by a modal drag. New left presses, including
* injected ones, clear this pending release before it can suppress their button-up. */
/** End the initiating press without a click and suppress its next backend button-up. */
void suppressNativeDragRelease();
/** Consume the one synthetic or delayed release left by a native drag. New left presses,
* including injected ones, clear this pending release before it can suppress their button-up.
*/
bool consumeNativeDragRelease();
/** Record a native, injected or touch press; a new left press ends any stale modal release. */
/** Record a native, injected or touch press; a new left press ends any stale native release. */
void recordButtonPress( const Uint32& Button );
EE::Window::Window* mWindow;
+2 -1
View File
@@ -1749,7 +1749,8 @@ solution "eepp"
files { "src/tests/native_drag_and_drop/main.cpp",
"src/examples/native_drag_and_drop/native_drag_and_drop.cpp" }
if os.is("linux") then
files { "src/tests/native_drag_and_drop/x11.cpp" }
files { "src/tests/native_drag_and_drop/x11.cpp",
"src/tests/native_drag_and_drop/wayland.cpp" }
links { "dl" }
elseif os.is("macosx") then
files { "src/tests/native_drag_and_drop/macos.cpp" }
+2 -1
View File
@@ -1776,7 +1776,8 @@ workspace "eepp"
files { "src/tests/native_drag_and_drop/main.cpp",
"src/examples/native_drag_and_drop/native_drag_and_drop.cpp" }
if os.istarget("linux") then
files { "src/tests/native_drag_and_drop/x11.cpp" }
files { "src/tests/native_drag_and_drop/x11.cpp",
"src/tests/native_drag_and_drop/wayland.cpp" }
links { "dl" }
elseif os.istarget("macosx") then
files { "src/tests/native_drag_and_drop/macos.cpp" }
+299
View File
@@ -0,0 +1,299 @@
#!/usr/bin/env python3
"""Run the explicit Wayland drag checks in Weston inside an isolated Xvfb display.
Requires Weston with its X11 backend, Xvfb, xdotool and ImageMagick's import.
Build eepp-native-dnd-tests first. Normal/manual runs never inject input.
"""
import argparse
import io
import os
import re
from pathlib import Path
import selectors
import shutil
import signal
import subprocess
import sys
import tempfile
import time
ROOT = Path(__file__).resolve().parents[2]
def read_line(process, prefix, timeout=25):
deadline = time.monotonic() + timeout
# Unbuffered binary stdout avoids hidden read-ahead when selecting a pipe.
with selectors.DefaultSelector() as selector:
selector.register(process.stdout, selectors.EVENT_READ)
pending = bytearray()
while time.monotonic() < deadline:
if not selector.select(max(0, deadline - time.monotonic())):
break
byte = process.stdout.read(1)
if not byte:
raise RuntimeError(f"Test process exited with {process.poll()}")
if byte != b"\n":
pending += byte
continue
line = pending.decode(errors="replace").strip()
pending.clear()
print(line, flush=True)
if line.startswith(prefix):
return line
raise RuntimeError(f"Timed out waiting for {prefix}")
def pointer(*arguments):
subprocess.run(["xdotool", *map(str, arguments)], check=True)
def color_point(color, compact=False):
# PPM keeps this runner independent of Python image packages.
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
stream = io.BytesIO(
subprocess.check_output(["import", "-window", "root", "-depth", "8", "ppm:-"])
)
assert stream.readline().strip() == b"P6"
line = stream.readline()
while line.startswith(b"#"):
line = stream.readline()
width, height = map(int, line.split())
assert stream.readline().strip() == b"255"
pixels = stream.read()
wanted = bytes(color)
# Window centers need room for the drag threshold; badges need only a small solid patch.
radius = 2 if compact else 24
candidates = []
for y in range(60, height - 30, 6):
for x in range(40, width - 40, 6):
if all(
pixels[(yy * width + xx) * 3 : (yy * width + xx) * 3 + 3] == wanted
for xx, yy in (
(x, y),
(x - radius, y),
(x + radius, y),
(x, y + min(radius, 20)),
)
):
if compact:
return x, y
candidates.append((x, y))
if candidates:
# Prefer the middle of the visible window, keeping the preview away from output edges.
center_x = (min(x for x, _ in candidates) + max(x for x, _ in candidates)) / 2
center_y = (min(y for _, y in candidates) + max(y for _, y in candidates)) / 2
return min(candidates, key=lambda p: (p[0] - center_x) ** 2 + (p[1] - center_y) ** 2)
time.sleep(0.1)
raise RuntimeError(f"No visible test surface with color {color}")
def run(binary, log_dir, scale):
runtime = tempfile.mkdtemp(prefix="eepp-weston-")
environment = dict(
os.environ,
XDG_RUNTIME_DIR=runtime,
WAYLAND_DISPLAY="eepp-dnd-test",
SDL_VIDEO_DRIVER="wayland",
SDL_VIDEODRIVER="wayland",
LIBGL_ALWAYS_SOFTWARE="1",
SDL_VIDEO_WAYLAND_ALLOW_LIBDECOR="0",
)
children = []
files = []
def launch(arguments, name, protocol=False):
log = (log_dir / (name + ".log")).open("wb")
files.append(log)
child_env = dict(environment)
if protocol:
child_env["WAYLAND_DEBUG"] = "1"
# Only Weston uses X11. The applications must operate as pure Wayland clients.
child_env.pop("DISPLAY", None)
child_env.pop("XAUTHORITY", None)
child = subprocess.Popen(
arguments,
cwd=ROOT,
env=child_env,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=log,
bufsize=0,
)
children.append(child)
return child
try:
weston = launch(
[
"weston",
"--backend=x11",
"--renderer=pixman",
f"--width={1240 // scale}",
f"--height={760 // scale}",
f"--scale={scale}",
"--socket=eepp-dnd-test",
"--no-config",
"--idle-time=0",
"--log=" + str(log_dir / "weston.log"),
],
"weston-stderr",
)
deadline = time.monotonic() + 10
while not Path(runtime, "eepp-dnd-test").exists():
if weston.poll() is not None or time.monotonic() > deadline:
raise RuntimeError("Weston could not start; inspect its log")
time.sleep(0.1)
tests = launch([str(binary), "--automated-wayland"], "source-protocol", True)
for scenario in (
"snapshot",
"large",
"rejected",
"cancel",
"escape",
"destroy",
"external",
"instant",
"stalled-cancel",
"shutdown",
):
ready = read_line(tests, "WAYLAND_DND_READY")
assert ready.endswith(scenario), ready
external = None
if scenario == "external":
external = launch(
[str(binary), "--automated-wayland-target"], "target-protocol", True
)
read_line(external, "WAYLAND_DND_EXTERNAL_READY")
time.sleep(0.25)
source = color_point((180, 30, 50))
pointer("mousemove", *source, "click", 1)
time.sleep(0.2)
source = color_point((180, 30, 50))
target = color_point((135, 45, 185) if external else (30, 80, 190))
pointer("mousemove", *source, "mousedown", 1)
time.sleep(0.1)
pointer("mousemove", source[0] + 18, source[1] + 4)
if scenario == "stalled-cancel":
read_line(tests, "WAYLAND_DND_BEFORE_START")
# Stop Weston before native setup, ensuring cancellation cannot use a cached reply.
# Resume it before rendering: swap/frame callbacks may legitimately wait for it.
weston.send_signal(signal.SIGSTOP)
try:
tests.stdin.write(b"begin\n")
tests.stdin.flush()
read_line(tests, "WAYLAND_DND_STARTED", timeout=2)
tests.stdin.write(b"cleanup\n")
tests.stdin.flush()
read_line(tests, "WAYLAND_DND_CLEANUP_DONE", timeout=2)
finally:
weston.send_signal(signal.SIGCONT)
else:
read_line(tests, "WAYLAND_DND_STARTED")
if scenario == "escape":
pointer("key", "Escape")
elif scenario == "rejected":
pointer("mousemove", 20, 600)
color_point((195, 45, 55), compact=True)
elif scenario not in ("cancel", "destroy", "instant", "stalled-cancel", "shutdown"):
pointer("mousemove", *target)
time.sleep(0.2)
color_point((31, 150, 82), compact=True)
if scenario == "snapshot":
pointer("mousemove", 20, 600)
color_point((195, 45, 55), compact=True)
pointer("mousemove", *target)
color_point((31, 150, 82), compact=True)
if scenario in ("snapshot", "external"):
color_point((40, 220, 100))
subprocess.run(
["import", "-window", "root", str(log_dir / "snapshot.png")], check=True
)
pointer("mouseup", 1)
result = read_line(tests, "WAYLAND_DND_RESULT")
assert " PASS " in result, result
if external:
assert read_line(external, "WAYLAND_DND_EXTERNAL ").endswith("PASS")
assert external.wait(timeout=5) == 0
time.sleep(0.1)
if scenario not in ("destroy", "shutdown"):
tests.stdin.write(b"click\n")
tests.stdin.flush()
read_line(tests, "WAYLAND_DND_CLICK_READY")
pointer("mousemove", *color_point((180, 30, 50)), "click", 1)
assert read_line(tests, "WAYLAND_DND_CLICK ").endswith("PASS")
if scenario != "shutdown":
tests.stdin.write(b"next\n")
tests.stdin.flush()
assert tests.wait(timeout=5) == 0
exiting = launch([str(binary), "--automated-wayland-exit"], "process-exit-protocol", True)
read_line(exiting, "WAYLAND_DND_READY shutdown")
time.sleep(0.25)
source = color_point((180, 30, 50))
pointer("mousemove", *source, "click", 1)
time.sleep(0.2)
source = color_point((180, 30, 50))
pointer("mousemove", *source, "mousedown", 1)
time.sleep(0.1)
pointer("mousemove", source[0] + 18, source[1] + 4)
read_line(exiting, "WAYLAND_DND_PROCESS_EXIT")
assert exiting.wait(timeout=5) == 0
pointer("mouseup", 1)
print("Wayland process exit with a live backend passed")
protocol = (log_dir / "source-protocol.log").read_text(errors="replace")
created = len(re.findall(r"\.data_offer\(new id wl_data_offer#", protocol))
destroyed = len(re.findall(r"-> wl_data_offer#\d+\.destroy\(\)", protocol))
assert created > 0 and created == destroyed, (created, destroyed)
print(f"Wayland offer proxies: {created} created, {destroyed} destroyed")
print("Wayland drag checks passed; logs:", log_dir)
except Exception:
subprocess.run(["import", "-window", "root", str(log_dir / "failure.png")], check=False)
raise
finally:
subprocess.run(
["xdotool", "mouseup", "1"],
check=False,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
for child in reversed(children):
if child.poll() is None:
child.terminate()
for child in children:
try:
child.wait(timeout=3)
except subprocess.TimeoutExpired:
child.kill()
child.wait()
for file in files:
file.close()
shutil.rmtree(runtime)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--inside-xvfb", action="store_true", help=argparse.SUPPRESS)
parser.add_argument("--binary", type=Path, default=ROOT / "bin/eepp-native-dnd-tests")
parser.add_argument("--log-dir", type=Path)
parser.add_argument("--scale", type=int, choices=(1, 2), default=1)
args = parser.parse_args()
if not args.inside_xvfb:
return subprocess.call(
[
str(ROOT / "projects/scripts/xvfb-run-eepp"),
sys.executable,
str(Path(__file__).resolve()),
"--inside-xvfb",
*sys.argv[1:],
]
)
log_dir = (args.log_dir or Path(tempfile.mkdtemp(prefix="eepp-wayland-dnd-logs-"))).resolve()
log_dir.mkdir(parents=True, exist_ok=True)
print("Wayland test logs:", log_dir, flush=True)
run(args.binary.resolve(), log_dir, args.scale)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -2,6 +2,8 @@
#ifdef EE_BACKEND_SDL2
#include <eepp/window/draganddrop/waylanddraganddrop.hpp>
#if ( EE_PLATFORM == EE_PLATFORM_ANDROID || EE_PLATFORM == EE_PLATFORM_IOS ) && defined( main )
#undef main
#endif
@@ -34,6 +36,9 @@ WindowBackendSDL2::WindowBackendSDL2() : WindowBackendLibrary() {
}
WindowBackendSDL2::~WindowBackendSDL2() {
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
Private::WaylandDragAndDrop::shutdown();
#endif
#if EE_PLATFORM != EE_PLATFORM_MACOS
SDL_Quit();
#endif
+2 -4
View File
@@ -591,10 +591,9 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
}
}
#if EE_PLATFORM == EE_PLATFORM_WIN
if ( event.Type == InputEvent::MouseButtonUp && event.button.button == EE_BUTTON_LEFT ) {
// OLE can consume the release before SDL notices it. Consume at most one delayed
// release after modal cleanup; a new left press clears the flag in Input.
// A native drag can synthesize a release on focus loss or consume the real release.
// Suppress only that gesture; a new left press clears the flag in Input.
auto* eventWindow = event.WinID == mWindow->getWindowID()
? mWindow
: Engine::instance()->getWindowID( event.WinID );
@@ -602,7 +601,6 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
Private::NativeDragAccess::consumeNativeDragRelease( *eventWindow->getInput() ) )
return;
}
#endif
if ( event.Type == InputEvent::MouseMotion )
Private::DragManager::handleInput( event.WinID, Private::NativeDragInput::Motion );
+9 -1
View File
@@ -18,6 +18,7 @@
#include <eepp/window/backend/SDL2/wminfo.hpp>
#include <eepp/window/backend/backendhelper.hpp>
#include <eepp/window/draganddrop/dragmanager.hpp>
#include <eepp/window/draganddrop/waylanddraganddrop.hpp>
#include <eepp/window/draganddrop/x11draganddrop.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/runtime.hpp>
@@ -45,7 +46,7 @@ NativeWindowContext WindowSDL::getNativeDragContext() {
if ( !driver )
return context;
#if EE_PLATFORM == EE_PLATFORM_WIN || EE_PLATFORM == EE_PLATFORM_MACOS || \
defined( EE_HAS_X11_DRAG_SOURCE )
defined( EE_HAS_X11_DRAG_SOURCE ) || defined( EE_HAS_WAYLAND_DRAG_SOURCE )
SDL_SysWMinfo info{};
SDL_VERSION( &info.version );
if ( !SDL_GetWindowWMInfo( mSDLWindow, &info ) )
@@ -151,6 +152,9 @@ WindowSDL::~WindowSDL() {
void WindowSDL::destroySDLResources() {
Private::DragManager::invalidateWindow( this );
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
Private::WaylandDragAndDrop::detachWindow( *this );
#endif
if ( nullptr != mGLContext ) {
SDL_GL_DeleteContext( mGLContext );
mGLContext = nullptr;
@@ -408,6 +412,10 @@ bool WindowSDL::create( WindowSettings Settings, ContextSettings Context ) {
if ( mWindow.WindowConfig.Style & WindowStyle::Hidden )
SDL_HideWindow( mSDLWindow );
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
Private::WaylandDragAndDrop::attachWindow( getNativeDragContext() );
#endif
logSuccessfulInit( getVersion() );
return true;
@@ -2,6 +2,8 @@
#ifdef EE_BACKEND_SDL3
#include <eepp/window/draganddrop/waylanddraganddrop.hpp>
#if ( EE_PLATFORM == EE_PLATFORM_ANDROID || EE_PLATFORM == EE_PLATFORM_IOS ) && defined( main )
#undef main
#endif
@@ -29,6 +31,9 @@ WindowBackendSDL3::WindowBackendSDL3() : WindowBackendLibrary() {
}
WindowBackendSDL3::~WindowBackendSDL3() {
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
Private::WaylandDragAndDrop::shutdown();
#endif
#if EE_PLATFORM != EE_PLATFORM_MACOS
SDL_Quit();
#endif
+2 -4
View File
@@ -571,10 +571,9 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
}
}
#if EE_PLATFORM == EE_PLATFORM_WIN
if ( event.Type == InputEvent::MouseButtonUp && event.button.button == EE_BUTTON_LEFT ) {
// OLE can consume the release before SDL notices it. Consume at most one delayed
// release after modal cleanup; a new left press clears the flag in Input.
// A native drag can synthesize a release on focus loss or consume the real release.
// Suppress only that gesture; a new left press clears the flag in Input.
auto* eventWindow = event.WinID == mWindow->getWindowID()
? mWindow
: Engine::instance()->getWindowID( event.WinID );
@@ -582,7 +581,6 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) {
Private::NativeDragAccess::consumeNativeDragRelease( *eventWindow->getInput() ) )
return;
}
#endif
if ( event.Type == InputEvent::MouseMotion )
Private::DragManager::handleInput( event.WinID, Private::NativeDragInput::Motion );
@@ -18,6 +18,7 @@
#include <eepp/window/backend/SDL3/wminfo.hpp>
#include <eepp/window/backend/backendhelper.hpp>
#include <eepp/window/draganddrop/dragmanager.hpp>
#include <eepp/window/draganddrop/waylanddraganddrop.hpp>
#include <eepp/window/draganddrop/x11draganddrop.hpp>
#include <eepp/window/engine.hpp>
#include <eepp/window/runtime.hpp>
@@ -133,6 +134,9 @@ WindowSDL::~WindowSDL() {
void WindowSDL::destroySDLResources() {
Private::DragManager::invalidateWindow( this );
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
Private::WaylandDragAndDrop::detachWindow( *this );
#endif
if ( nullptr != mGLContext ) {
SDL_GL_DestroyContext( mGLContext );
mGLContext = nullptr;
@@ -366,6 +370,10 @@ bool WindowSDL::create( WindowSettings Settings, ContextSettings Context ) {
if ( mWindow.WindowConfig.Style & WindowStyle::Hidden )
SDL_HideWindow( mSDLWindow );
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
Private::WaylandDragAndDrop::attachWindow( getNativeDragContext() );
#endif
logSuccessfulInit( getVersion() );
return true;
@@ -35,6 +35,8 @@ struct NativeDragAccess {
static void resetNativeDragInput( Input& input ) { input.resetNativeDragInput(); }
static void suppressNativeDragRelease( Input& input ) { input.suppressNativeDragRelease(); }
static bool consumeNativeDragRelease( Input& input ) {
return input.consumeNativeDragRelease();
}
+24 -2
View File
@@ -1,6 +1,7 @@
#include "dragmanager.hpp"
#include "cocoadraganddrop.hpp"
#include "dragaccess.hpp"
#include "waylanddraganddrop.hpp"
#include "win32draganddrop.hpp"
#include "x11draganddrop.hpp"
#include <eepp/window/engine.hpp>
@@ -21,9 +22,15 @@ bool DragManager::isFileDragSupported( const NativeWindowContext& context ) {
return context.kind == NativeWindowContext::Kind::Win32;
#elif EE_PLATFORM == EE_PLATFORM_MACOS
return context.kind == NativeWindowContext::Kind::Cocoa;
#elif defined( EE_HAS_X11_DRAG_SOURCE )
return context.kind == NativeWindowContext::Kind::X11 && X11DragAndDrop::isSupported();
#else
#ifdef EE_HAS_X11_DRAG_SOURCE
if ( context.kind == NativeWindowContext::Kind::X11 )
return X11DragAndDrop::isSupported();
#endif
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
if ( context.kind == NativeWindowContext::Kind::Wayland )
return WaylandDragAndDrop::isSupported( context );
#endif
return false;
#endif
}
@@ -32,6 +39,12 @@ DragStartResult DragManager::startFileDrag( NativeWindowContext context, const D
std::string payload,
DragCompletionCallback completion ) {
DragStartResult result;
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
if ( context.kind == NativeWindowContext::Kind::Wayland ) {
sSource = WaylandDragAndDrop::createSource( context, offer, std::move( payload ),
std::move( completion ), result );
}
#endif
#ifdef EE_HAS_X11_DRAG_SOURCE
if ( context.kind == NativeWindowContext::Kind::X11 ) {
// Enable backend protocol forwarding only for a session, so idle windows do not
@@ -116,4 +129,13 @@ void DragManager::invalidateWindow( Window* window ) {
}
}
void DragManager::shutdown() {
if ( !sSource )
return;
auto source = std::move( sSource );
source->cancel();
if ( source->owner )
NativeDragAccess::shutdownNativeDragEvents( *source->owner );
}
}}} // namespace EE::Window::Private
@@ -33,6 +33,9 @@ class DragManager {
static void invalidateWindow( Window* window );
/** Release the session without delivering its completion during backend/process teardown. */
static void shutdown();
private:
static std::unique_ptr<DragSource> sSource;
static bool sPolling;
@@ -64,4 +64,25 @@ Image NativeDragPreview::makeCopyCursorImage( CopyCursorPixels& pixels ) {
return image;
}
Image NativeDragPreview::makeFeedbackImage( FeedbackImagePixels& pixels, DragFeedback feedback ) {
Image image( pixels.data(), 16, 16, 4 );
image.avoidFreeImage( true );
image.fillWithColor( Color::Transparent );
const Color background = feedback == DragFeedback::Copy ? Color( 31, 150, 82 )
: feedback == DragFeedback::Rejected ? Color( 195, 45, 55 )
: Color( 190, 135, 20 );
for ( unsigned y = 1; y < 15; ++y ) {
for ( unsigned x = 1; x < 15; ++x ) {
const bool horizontal = y >= 7 && y <= 8 && x >= 4 && x <= 11;
const bool vertical = x >= 7 && x <= 8 && y >= 4 && y <= 11;
const bool dots = y >= 7 && y <= 8 && x >= 3 && x <= 12 && ( x - 3 ) % 4 < 2;
const bool mark = feedback == DragFeedback::Pending
? dots
: horizontal || ( feedback == DragFeedback::Copy && vertical );
image.setPixel( x, y, mark ? Color::White : background );
}
}
return image;
}
}}} // namespace EE::Window::Private
+4 -1
View File
@@ -13,7 +13,7 @@ namespace EE { namespace Window { namespace Private {
enum class DragFeedback : Uint8 { Rejected, Pending, Copy };
/** Borrowed backend presentation for the single process-wide drag. The transport must reset it
/** Backend presentation for the single process-wide drag. The transport must reset it
* before releasing its source window. Backend resources never outlive reset(). */
class NativeDragPreview {
public:
@@ -30,11 +30,14 @@ class NativeDragPreview {
protected:
using FileImagePixels = std::array<Uint8, 112 * 48 * 4>;
using CopyCursorPixels = std::array<Uint8, 24 * 24 * 4>;
using FeedbackImagePixels = std::array<Uint8, 16 * 16 * 4>;
/** Images borrow the supplied scratch storage and must not outlive it. */
static Graphics::Image makeFileImage( FileImagePixels& pixels, size_t count );
static Graphics::Image makeCopyCursorImage( CopyCursorPixels& pixels );
static Graphics::Image makeFeedbackImage( FeedbackImagePixels& pixels, DragFeedback feedback );
};
}}} // namespace EE::Window::Private
@@ -0,0 +1,627 @@
#include "waylanddraganddrop.hpp"
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
#include "dragaccess.hpp"
#include "dragmanager.hpp"
#include "waylanddragpreview.hpp"
#include "waylandlibrary.hpp"
#include <array>
#include <atomic>
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <eepp/window/input.hpp>
#include <eepp/window/window.hpp>
#include <fcntl.h>
#include <poll.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
namespace EE { namespace Window { namespace Private {
namespace {
constexpr uint32_t leftButton = 0x110; // Linux input button codes are part of the Wayland protocol.
constexpr const char* fileMime = "text/uri-list";
struct WaylandSeat {
WaylandLibrary& library;
wl_seat* seat{};
wl_pointer* pointer{};
wl_surface* focus{};
wl_surface* origin{};
uint32_t global{}, serial{}, buttons{};
explicit WaylandSeat( WaylandLibrary& library ) : library( library ) {}
~WaylandSeat() {
clearPointer();
if ( seat && library.version( seat ) >= WL_SEAT_RELEASE_SINCE_VERSION )
library.destroy( seat, WL_SEAT_RELEASE );
else
library.releaseProxy( seat );
}
void clearPointer() {
if ( pointer && library.version( pointer ) >= WL_POINTER_RELEASE_SINCE_VERSION )
library.destroy( pointer, WL_POINTER_RELEASE );
else
library.releaseProxy( pointer );
focus = origin = nullptr;
serial = buttons = 0;
}
static void capabilities( void* data, wl_seat*, uint32_t capabilities ) {
auto& self = *static_cast<WaylandSeat*>( data );
if ( !( capabilities & WL_SEAT_CAPABILITY_POINTER ) ) {
self.clearPointer();
return;
}
if ( self.pointer )
return;
self.pointer =
self.library.create<wl_pointer>( self.seat, WL_SEAT_GET_POINTER, self.library.pointer );
static const wl_pointer_listener listener = [] {
wl_pointer_listener result{};
result.enter = []( void* data, wl_pointer*, uint32_t, wl_surface* surface, wl_fixed_t,
wl_fixed_t ) { static_cast<WaylandSeat*>( data )->focus = surface; };
result.leave = []( void* data, wl_pointer*, uint32_t, wl_surface* ) {
auto& self = *static_cast<WaylandSeat*>( data );
self.focus = self.origin = nullptr;
self.serial = self.buttons = 0;
};
result.motion = []( void*, wl_pointer*, uint32_t, wl_fixed_t, wl_fixed_t ) {};
result.button = []( void* data, wl_pointer*, uint32_t serial, uint32_t, uint32_t button,
uint32_t state ) {
auto& self = *static_cast<WaylandSeat*>( data );
if ( state == WL_POINTER_BUTTON_STATE_PRESSED ) {
if ( button == leftButton && self.buttons == 0 ) {
self.serial = serial;
self.origin = self.focus;
}
++self.buttons;
} else {
if ( self.buttons )
--self.buttons;
if ( button == leftButton ) {
self.serial = 0;
self.origin = nullptr;
}
}
};
result.axis = []( void*, wl_pointer*, uint32_t, uint32_t, wl_fixed_t ) {};
result.frame = []( void*, wl_pointer* ) {};
result.axis_source = []( void*, wl_pointer*, uint32_t ) {};
result.axis_stop = []( void*, wl_pointer*, uint32_t, uint32_t ) {};
result.axis_discrete = []( void*, wl_pointer*, uint32_t, int32_t ) {};
return result;
}();
if ( self.pointer )
self.library.listen( self.pointer, listener, &self );
}
};
} // namespace
/** One observer on SDL's connection. Additional pointer resources receive the same real press
* serial without replacing SDL's listeners or borrowing private SDL structures. */
struct WaylandConnection {
struct PendingDevice {
wl_data_device* device{};
wl_callback* callback{};
};
WaylandLibrary library;
SmallVector<std::unique_ptr<WaylandSeat>, 1> seats;
SmallVector<std::pair<const Window*, wl_surface*>, 2> windows;
SmallVector<PendingDevice, 1> pendingDevices;
SmallVector<wl_data_offer*, 2> offers;
wl_display* display{};
wl_registry* registry{};
wl_compositor* compositor{};
wl_subcompositor* subcompositor{};
wl_shm* shm{};
wl_data_device_manager* manager{};
uint32_t compositorGlobal{}, subcompositorGlobal{}, shmGlobal{}, managerGlobal{};
bool dragging{};
void reset() {
endDrag();
for ( auto& pending : pendingDevices ) {
library.releaseProxy( pending.callback );
library.releaseProxy( pending.device ); // Its server resource was already released.
}
pendingDevices.clear();
windows.clear();
seats.clear();
library.releaseProxy( manager );
library.releaseProxy( shm );
library.releaseProxy( compositor );
library.destroy( subcompositor, WL_SUBCOMPOSITOR_DESTROY );
library.releaseProxy( registry );
display = nullptr;
compositorGlobal = subcompositorGlobal = shmGlobal = managerGlobal = 0;
}
static void global( void* data, wl_registry* registry, uint32_t name, const char* interface,
uint32_t version ) {
auto& self = *static_cast<WaylandConnection*>( data );
auto& lib = self.library;
if ( std::strcmp( interface, "wl_compositor" ) == 0 && version >= 3 ) {
self.compositor =
lib.bind<wl_compositor>( registry, name, lib.compositor, std::min( version, 4u ) );
self.compositorGlobal = name;
} else if ( std::strcmp( interface, "wl_subcompositor" ) == 0 ) {
self.subcompositor = lib.bind<wl_subcompositor>( registry, name, lib.subcompositor, 1 );
self.subcompositorGlobal = name;
} else if ( std::strcmp( interface, "wl_shm" ) == 0 ) {
self.shm = lib.bind<wl_shm>( registry, name, lib.shm, 1 );
self.shmGlobal = name;
static constexpr wl_shm_listener listener{ []( void*, wl_shm*, uint32_t ) {} };
if ( self.shm )
lib.listen( self.shm, listener, nullptr );
} else if ( std::strcmp( interface, "wl_data_device_manager" ) == 0 && version >= 3 ) {
self.manager =
lib.bind<wl_data_device_manager>( registry, name, lib.data_device_manager, 3 );
self.managerGlobal = name;
} else if ( std::strcmp( interface, "wl_seat" ) == 0 ) {
// A seat's callback address must remain stable when another seat is hot-plugged.
auto seat = std::make_unique<WaylandSeat>( lib );
seat->global = name;
seat->seat = lib.bind<wl_seat>( registry, name, lib.seat, std::min( version, 5u ) );
static constexpr wl_seat_listener listener{ WaylandSeat::capabilities,
[]( void*, wl_seat*, const char* ) {} };
if ( seat->seat ) {
lib.listen( seat->seat, listener, seat.get() );
self.seats.push_back( std::move( seat ) );
}
}
}
static void removed( void* data, wl_registry*, uint32_t name ) {
auto& self = *static_cast<WaylandConnection*>( data );
for ( auto it = self.seats.begin(); it != self.seats.end(); ++it ) {
if ( ( *it )->global == name ) {
self.seats.erase( it );
break;
}
}
if ( name == self.managerGlobal )
self.library.releaseProxy( self.manager );
if ( name == self.compositorGlobal )
self.library.releaseProxy( self.compositor );
if ( name == self.subcompositorGlobal )
self.library.destroy( self.subcompositor, WL_SUBCOMPOSITOR_DESTROY );
if ( name == self.shmGlobal )
self.library.releaseProxy( self.shm );
}
bool initialize( wl_display* connection ) {
if ( !library.load() )
return false;
display = connection;
registry =
library.create<wl_registry>( display, WL_DISPLAY_GET_REGISTRY, library.registry );
if ( !registry )
return false;
static constexpr wl_registry_listener listener{ global, removed };
library.listen( registry, listener, this );
// Setup only, never per drag/motion. The second barrier delivers seat capabilities so its
// pointer listener is in place before the application handles its first gesture.
if ( library.wl_display_roundtrip( display ) < 0 ||
library.wl_display_roundtrip( display ) < 0 )
return false;
library.wl_display_flush( display );
return true;
}
bool startDrag( wl_data_source* source, wl_surface* origin, wl_surface* icon,
const WaylandSeat& seat ) {
auto* device = library.create<wl_data_device>(
manager, WL_DATA_DEVICE_MANAGER_GET_DATA_DEVICE, library.data_device, seat.seat );
if ( !device )
return false;
// This short-lived device must never replace SDL's receiver. Its events stay on the
// connection's UI queue: even a late setup reply cannot access a destroyed session.
static constexpr wl_data_device_listener deviceListener{
[]( void* data, wl_data_device*, wl_data_offer* offer ) {
auto& self = *static_cast<WaylandConnection*>( data );
static constexpr wl_data_offer_listener listener{
[]( void*, wl_data_offer*, const char* ) {},
[]( void*, wl_data_offer*, uint32_t ) {},
[]( void*, wl_data_offer*, uint32_t ) {} };
self.library.listen( offer, listener, nullptr );
if ( self.dragging )
self.offers.push_back( offer );
else
self.library.destroy( offer, WL_DATA_OFFER_DESTROY );
},
[]( void*, wl_data_device*, uint32_t, wl_surface*, wl_fixed_t, wl_fixed_t,
wl_data_offer* ) {},
[]( void*, wl_data_device* ) {},
[]( void*, wl_data_device*, uint32_t, wl_fixed_t, wl_fixed_t ) {},
[]( void*, wl_data_device* ) {},
[]( void*, wl_data_device*, wl_data_offer* ) {} };
library.listen( device, deviceListener, this );
dragging = true;
library.request( device, WL_DATA_DEVICE_START_DRAG, source, origin, icon, seat.serial );
library.request( device, WL_DATA_DEVICE_RELEASE );
auto* callback = library.create<wl_callback>( display, WL_DISPLAY_SYNC, library.callback );
pendingDevices.push_back( { device, callback } );
static constexpr wl_callback_listener releaseListener{
[]( void* data, wl_callback* callback, uint32_t ) {
auto& self = *static_cast<WaylandConnection*>( data );
for ( auto it = self.pendingDevices.begin(); it != self.pendingDevices.end();
++it ) {
if ( it->callback == callback ) {
self.library.releaseProxy( it->device );
self.library.releaseProxy( it->callback );
self.pendingDevices.erase( it );
break;
}
}
} };
if ( callback )
library.listen( callback, releaseListener, this );
return callback != nullptr;
}
void endDrag() {
dragging = false;
// Destroy the source first: discarding its current offer while it is live may cancel it.
// Offers arriving after cancellation are disposed of by the connection listener above.
for ( auto* offer : offers )
library.destroy( offer, WL_DATA_OFFER_DESTROY );
offers.clear();
}
bool supports( const NativeWindowContext& context ) const {
return context.kind == NativeWindowContext::Kind::Wayland &&
display == context.handles.wayland.display && compositor && subcompositor && shm &&
manager && std::any_of( seats.begin(), seats.end(), []( const auto& seat ) {
return seat->global && seat->pointer;
} );
}
};
WaylandConnection WaylandDragAndDrop::sConnection;
bool WaylandDragAndDrop::sExitHandlerRegistered = false;
namespace {
class WaylandDragSource final : public DragSource {
public:
WaylandDragSource( WaylandConnection& connection, NativeWindowContext context,
std::string payload, DragCompletionCallback completion ) :
DragSource( context.owner, std::move( completion ) ),
connection( connection ),
payload( std::move( payload ) ),
preview( connection.library ),
origin( static_cast<wl_surface*>( context.handles.wayland.surface ) ) {}
~WaylandDragSource() override { reset(); }
DragStartResult begin( const DragOffer& offer, const WaylandSeat& seat ) {
seatGlobal = seat.global;
auto& lib = connection.library;
queue = lib.wl_display_create_queue( connection.display );
if ( !queue || pipe( wake ) != 0 )
return { "Could not create the Wayland transfer queue", DragStartStatus::Failed };
for ( const auto fd : wake ) {
if ( fcntl( fd, F_SETFD, FD_CLOEXEC ) < 0 || fcntl( fd, F_SETFL, O_NONBLOCK ) < 0 )
return { "Could not configure the transfer wake pipe", DragStartStatus::Failed };
}
// Assign the source queue through a wrapper before constructing the source. The manager
// remains on the UI queue for its short-lived initiating devices.
auto* managerProxy =
static_cast<wl_proxy*>( lib.wl_proxy_create_wrapper( connection.manager ) );
if ( !managerProxy )
return { "Could not create the Wayland data source", DragStartStatus::Failed };
lib.wl_proxy_set_queue( managerProxy, queue );
source = lib.create<wl_data_source>(
managerProxy, WL_DATA_DEVICE_MANAGER_CREATE_DATA_SOURCE, lib.data_source );
lib.wl_proxy_wrapper_destroy( managerProxy );
if ( !source )
return { "Could not create the Wayland data source", DragStartStatus::Failed };
static constexpr wl_data_source_listener sourceListener{
[]( void* data, wl_data_source*, const char* mime ) {
static_cast<WaylandDragSource*>( data )->accepted =
mime && std::strcmp( mime, fileMime ) == 0;
},
send,
[]( void* data, wl_data_source* ) {
static_cast<WaylandDragSource*>( data )->cancelled = true;
},
[]( void* data, wl_data_source* ) {
static_cast<WaylandDragSource*>( data )->dropped = true;
},
[]( void* data, wl_data_source* ) {
static_cast<WaylandDragSource*>( data )->finished = true;
},
[]( void* data, wl_data_source*, uint32_t action ) {
auto& self = *static_cast<WaylandDragSource*>( data );
self.action = action;
// A new destination may negotiate actions before accepting a MIME type. Do not
// carry the previous target's acceptance across the intervening None action.
if ( action == WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE )
self.accepted = false;
} };
lib.listen( source, sourceListener, this );
if ( !preview.initialize( connection.compositor, connection.subcompositor, connection.shm,
offer, owner->getScale() ) )
return { "Could not create the Wayland drag image", DragStartStatus::Failed };
lib.request( source, WL_DATA_SOURCE_OFFER, fileMime );
lib.request( source, WL_DATA_SOURCE_SET_ACTIONS, WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY );
started = true;
// SDL can synthesize button-up as soon as the compositor takes pointer focus. Arm the
// suppression before that event, including cancellation before the first event pump.
NativeDragAccess::suppressNativeDragRelease( *owner->getInput() );
if ( !connection.startDrag( source, origin, preview.surface(), seat ) )
return { "Could not start the Wayland drag", DragStartStatus::Failed };
lib.request( preview.surface(), WL_SURFACE_COMMIT );
// Session events use their own queue and transfer thread. SDL synchronously reads incoming
// drops: serving those on the UI queue would deadlock drops into another eepp window.
// Thread::launch() cannot report failure; reject the start if its transfer worker cannot
// run.
const int threadError = pthread_create( &worker, nullptr, run, this );
if ( threadError )
return { "Could not create the Wayland transfer thread", DragStartStatus::Failed };
workerStarted = true;
lib.wl_display_flush( connection.display );
return { {}, DragStartStatus::Started };
}
void tick() override {
if ( completed )
return;
preview.setFeedback( accepted ? ( action == WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY
? DragFeedback::Copy
: DragFeedback::Pending )
: DragFeedback::Rejected );
if ( failed || connection.library.wl_display_get_error( connection.display ) ) {
reset();
finish( DragEndReason::Failed, DragAction::None, "Wayland data transfer failed" );
} else if ( cancelled || !std::any_of( connection.seats.begin(), connection.seats.end(),
[this]( const auto& seat ) {
return seat->global == seatGlobal &&
seat->pointer;
} ) ) {
cancel();
} else if ( finished ) {
const bool copied = dropped && action == WL_DATA_DEVICE_MANAGER_DND_ACTION_COPY;
reset();
finish( copied ? DragEndReason::Dropped : DragEndReason::Cancelled,
copied ? DragAction::Copy : DragAction::None );
}
}
bool handleEvent( void* ) override { return false; }
void handleInput( NativeDragInput input ) override {
if ( input == NativeDragInput::Escape )
cancel();
}
void cancel() override {
reset();
finish( DragEndReason::Cancelled );
}
private:
struct Transfer {
size_t offset{};
int fd{ -1 };
};
WaylandConnection& connection;
std::string payload;
WaylandDragPreview preview;
wl_surface* origin{};
wl_data_source* source{};
wl_event_queue* queue{};
pthread_t worker{};
// Bounded concurrent receivers share the same immutable payload; no per-send copies.
std::array<Transfer, 16> transfers;
int wake[2]{ -1, -1 };
uint32_t seatGlobal{};
std::atomic<uint32_t> action{ WL_DATA_DEVICE_MANAGER_DND_ACTION_NONE };
std::atomic<bool> stopping{ false }, failed{ false }, cancelled{ false }, dropped{ false },
finished{ false }, accepted{ false };
bool workerStarted{ false }, started{ false };
static void send( void* data, wl_data_source*, const char* mime, int32_t fd ) {
auto& self = *static_cast<WaylandDragSource*>( data );
if ( !mime || std::strcmp( mime, fileMime ) != 0 || fcntl( fd, F_SETFD, FD_CLOEXEC ) < 0 ||
fcntl( fd, F_SETFL, O_NONBLOCK ) < 0 ) {
close( fd );
return;
}
for ( auto& transfer : self.transfers ) {
if ( transfer.fd < 0 ) {
transfer = { 0, fd };
return;
}
}
close( fd );
self.failed = true;
}
static void* run( void* data ) {
static_cast<WaylandDragSource*>( data )->transferLoop();
return nullptr;
}
void transferLoop() {
// Only this short-lived worker blocks SIGPIPE, never the UI thread or the process. A target
// may close its pipe at any point; an EPIPE must not terminate the application.
sigset_t pipeSignal;
sigemptyset( &pipeSignal );
sigaddset( &pipeSignal, SIGPIPE );
pthread_sigmask( SIG_BLOCK, &pipeSignal, nullptr );
auto& lib = connection.library;
auto* display = connection.display;
while ( !stopping && !failed ) {
if ( lib.wl_display_dispatch_queue_pending( display, queue ) < 0 ) {
failed = true;
break;
}
if ( lib.wl_display_prepare_read_queue( display, queue ) != 0 )
continue;
std::array<pollfd, 18> descriptors{};
descriptors[0] = { lib.wl_display_get_fd( display ), POLLIN, 0 };
descriptors[1] = { wake[0], POLLIN, 0 };
if ( lib.wl_display_flush( display ) < 0 ) {
if ( errno == EAGAIN )
descriptors[0].events |= POLLOUT;
else {
lib.wl_display_cancel_read( display );
failed = true;
break;
}
}
for ( size_t i = 0; i < transfers.size(); ++i )
descriptors[i + 2] = { transfers[i].fd, POLLOUT, 0 };
const int ready = poll( descriptors.data(), descriptors.size(), 100 );
if ( ready > 0 && ( descriptors[0].revents & POLLIN ) ) {
if ( lib.wl_display_read_events( display ) < 0 )
failed = true;
} else {
lib.wl_display_cancel_read( display );
}
if ( ( ready < 0 && errno != EINTR ) ||
( descriptors[0].revents & ( POLLERR | POLLHUP | POLLNVAL ) ) ) {
failed = true;
break;
}
for ( size_t i = 0; i < transfers.size(); ++i ) {
auto& transfer = transfers[i];
if ( transfer.fd < 0 || !descriptors[i + 2].revents )
continue;
const auto bytes =
write( transfer.fd, payload.data() + transfer.offset,
std::min<size_t>( payload.size() - transfer.offset, 65536 ) );
if ( bytes > 0 )
transfer.offset += bytes;
if ( transfer.offset == payload.size() ||
( bytes < 0 && errno != EAGAIN && errno != EINTR ) ) {
close( transfer.fd );
transfer.fd = -1;
}
}
}
}
void reset() {
auto& lib = connection.library;
if ( workerStarted ) {
stopping = true;
const char byte = 0;
const auto ignored = write( wake[1], &byte, 1 );
(void)ignored;
pthread_join( worker, nullptr );
workerStarted = false;
}
for ( auto& transfer : transfers ) {
if ( transfer.fd >= 0 )
close( transfer.fd );
transfer.fd = -1;
}
lib.destroy( source, WL_DATA_SOURCE_DESTROY );
connection.endDrag();
preview.reset();
if ( queue ) {
lib.wl_event_queue_destroy( queue );
queue = nullptr;
}
for ( auto& fd : wake ) {
if ( fd >= 0 )
close( fd );
fd = -1;
}
if ( started && owner )
owner->getInput()->resetButton( EE_BUTTON_LEFT );
started = false;
if ( connection.display )
lib.wl_display_flush( connection.display );
}
};
} // namespace
void WaylandDragAndDrop::attachWindow( const NativeWindowContext& context ) {
auto& connection = sConnection;
if ( context.kind != NativeWindowContext::Kind::Wayland )
return;
if ( !sExitHandlerRegistered ) {
// A caller may return from main or call std::exit without destroying Engine. Register
// after static initialization so sources and observers are released before their loader,
// independently of translation-unit destruction order. Normal backend teardown stays eager.
if ( std::atexit( shutdown ) != 0 )
return;
sExitHandlerRegistered = true;
}
auto* display = static_cast<wl_display*>( context.handles.wayland.display );
if ( connection.display && connection.display != display )
return;
connection.windows.emplace_back( context.owner,
static_cast<wl_surface*>( context.handles.wayland.surface ) );
if ( !connection.display && !connection.initialize( display ) )
connection.reset();
}
void WaylandDragAndDrop::detachWindow( const Window& window ) {
auto& connection = sConnection;
auto& windows = connection.windows;
for ( auto it = windows.begin(); it != windows.end(); ++it ) {
if ( it->first == &window ) {
for ( auto& seat : connection.seats ) {
if ( seat->focus == it->second )
seat->focus = nullptr;
if ( seat->origin == it->second ) {
seat->origin = nullptr;
seat->serial = seat->buttons = 0;
}
}
windows.erase( it );
break;
}
}
}
void WaylandDragAndDrop::shutdown() {
// wl_display.get_registry's server resources live until disconnect. Retain one observer
// across zero-window periods instead of adding a new registry for each reopening.
if ( sConnection.display ) {
DragManager::shutdown();
sConnection.reset();
}
}
bool WaylandDragAndDrop::isSupported( const NativeWindowContext& context ) {
return sConnection.supports( context );
}
std::unique_ptr<DragSource> WaylandDragAndDrop::createSource( NativeWindowContext context,
const DragOffer& offer,
std::string payload,
DragCompletionCallback completion,
DragStartResult& result ) {
const auto seat = std::find_if(
sConnection.seats.begin(), sConnection.seats.end(), [&context]( const auto& candidate ) {
return candidate->buttons == 1 && candidate->origin == context.handles.wayland.surface;
} );
if ( seat == sConnection.seats.end() || !context.owner->getInput()->isMouseLeftPressed() ) {
result = { "A real Wayland left-button press on the source window is required",
DragStartStatus::InvalidGesture };
return nullptr;
}
auto source = std::make_unique<WaylandDragSource>( sConnection, context, std::move( payload ),
std::move( completion ) );
result = source->begin( offer, **seat );
if ( result.status != DragStartStatus::Started )
return nullptr;
return source;
}
}}} // namespace EE::Window::Private
#endif
@@ -0,0 +1,42 @@
#ifndef EE_WINDOW_WAYLAND_DRAGANDDROP_HPP
#define EE_WINDOW_WAYLAND_DRAGANDDROP_HPP
#include "dragsource.hpp"
#include "waylandlibrary.hpp"
#include <eepp/window/nativewindowcontext.hpp>
#include <memory>
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
namespace EE { namespace Window { namespace Private {
struct WaylandConnection;
class WaylandDragAndDrop {
public:
WaylandDragAndDrop() = delete;
/** Observe the seat before its first press. Each successful attach needs one detach, before
* the backend destroys its window. Neither operation takes ownership of SDL's handles. */
static void attachWindow( const NativeWindowContext& context );
static void detachWindow( const Window& window );
/** Release connection-wide observers before the backend quits SDL. */
static void shutdown();
static bool isSupported( const NativeWindowContext& context );
static std::unique_ptr<DragSource> createSource( NativeWindowContext context,
const DragOffer& offer, std::string payload,
DragCompletionCallback completion,
DragStartResult& result );
private:
static WaylandConnection sConnection;
static bool sExitHandlerRegistered;
};
}}} // namespace EE::Window::Private
#endif
#endif
@@ -0,0 +1,168 @@
#include "waylanddragpreview.hpp"
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
#include <cmath>
#include <eepp/graphics/image.hpp>
#include <eepp/system/sys.hpp>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#if EE_PLATFORM == EE_PLATFORM_LINUX
#include <sys/syscall.h>
#endif
namespace EE { namespace Window { namespace Private {
WaylandDragPreview::~WaylandDragPreview() {
reset();
}
bool WaylandDragPreview::initialize( wl_compositor* compositor, wl_subcompositor* subcompositor,
wl_shm* shm, const DragOffer& offer, float scale ) {
FileImagePixels pixels;
Graphics::Image generated;
const auto* image = offer.preview.image;
if ( !image ) {
generated = makeFileImage( pixels, offer.files.size() );
image = &generated;
}
if ( !std::isfinite( scale ) || scale < 1 )
scale = 1;
const int bufferScale = static_cast<int>( std::ceil( scale ) );
const int inputWidth = image->getWidth(), inputHeight = image->getHeight();
const int width = static_cast<int>( std::ceil( inputWidth / scale ) ) * bufferScale;
const int height = static_cast<int>( std::ceil( inputHeight / scale ) ) * bufferScale;
const size_t imageBytes = static_cast<size_t>( width ) * height * 4;
const int badgeSize = 16 * bufferScale;
const size_t badgeBytes = static_cast<size_t>( badgeSize ) * badgeSize * 4;
const size_t bytes = imageBytes + feedbackBuffers.size() * badgeBytes;
// Prefer an anonymous, close-on-exec memory file on Linux without requiring a newer libc.
// BSD, older kernels and sandbox policies can use an unlinked, mode-0600 temp file instead.
int fd = -1;
#if EE_PLATFORM == EE_PLATFORM_LINUX && defined( SYS_memfd_create ) && defined( MFD_CLOEXEC )
fd = static_cast<int>( syscall( SYS_memfd_create, "eepp-drag-icon", MFD_CLOEXEC ) );
#endif
if ( fd < 0 ) {
auto path = System::Sys::getTempPath();
path += "eepp-drag-icon-XXXXXX";
fd = mkstemp( path.data() );
if ( fd < 0 )
return false;
if ( unlink( path.c_str() ) != 0 || fcntl( fd, F_SETFD, FD_CLOEXEC ) < 0 ) {
close( fd );
return false;
}
}
if ( ftruncate( fd, bytes ) != 0 ) {
close( fd );
return false;
}
// The compositor retains its own mapping; the local mapping is needed only for this upload.
auto* output =
static_cast<Uint32*>( mmap( nullptr, bytes, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0 ) );
if ( output == MAP_FAILED ) {
close( fd );
return false;
}
// Convert directly into shared memory. Fractional display scales need dimensions that are
// multiples of the integer buffer scale; no intermediate image is allocated.
auto upload = []( const Graphics::Image& image, Uint32* output, int width, int height ) {
const auto* pixelsIn = image.getPixels();
const int inputWidth = image.getWidth(), inputHeight = image.getHeight();
const auto channels = image.getChannels();
for ( int y = 0; y < height; ++y ) {
const auto* row =
pixelsIn + static_cast<size_t>( y * inputHeight / height ) * inputWidth * channels;
for ( int x = 0; x < width; ++x ) {
const auto* input = row + ( x * inputWidth / width ) * channels;
const Uint32 alpha = channels == 4 ? input[3] : 255;
output[static_cast<size_t>( y ) * width + x] =
alpha << 24 | ( input[0] * alpha / 255 ) << 16 |
( input[1] * alpha / 255 ) << 8 | input[2] * alpha / 255;
}
}
};
upload( *image, output, width, height );
FeedbackImagePixels badgePixels;
for ( size_t i = 0; i < feedbackBuffers.size(); ++i ) {
auto badgeImage = makeFeedbackImage( badgePixels, static_cast<DragFeedback>( i ) );
upload( badgeImage, output + ( imageBytes + i * badgeBytes ) / 4, badgeSize, badgeSize );
}
auto* pool = library.create<wl_shm_pool>( shm, WL_SHM_CREATE_POOL, library.shm_pool, fd,
static_cast<int>( bytes ) );
close( fd );
munmap( output, bytes );
if ( !pool )
return false;
buffer = library.create<wl_buffer>( pool, WL_SHM_POOL_CREATE_BUFFER, library.buffer, 0, width,
height, width * 4, WL_SHM_FORMAT_ARGB8888 );
for ( size_t i = 0; i < feedbackBuffers.size(); ++i ) {
feedbackBuffers[i] =
library.create<wl_buffer>( pool, WL_SHM_POOL_CREATE_BUFFER, library.buffer,
static_cast<int>( imageBytes + i * badgeBytes ), badgeSize,
badgeSize, badgeSize * 4, WL_SHM_FORMAT_ARGB8888 );
}
library.destroy( pool, WL_SHM_POOL_DESTROY );
icon = library.create<wl_surface>( compositor, WL_COMPOSITOR_CREATE_SURFACE, library.surface );
badge = library.create<wl_surface>( compositor, WL_COMPOSITOR_CREATE_SURFACE, library.surface );
if ( !buffer || !icon || !badge ||
std::any_of( feedbackBuffers.begin(), feedbackBuffers.end(),
[]( const auto* buffer ) { return !buffer; } ) )
return false;
badgeRole = library.create<wl_subsurface>( subcompositor, WL_SUBCOMPOSITOR_GET_SUBSURFACE,
library.subsurface, badge, icon );
auto* region =
library.create<wl_region>( compositor, WL_COMPOSITOR_CREATE_REGION, library.region );
if ( !badgeRole || !region ) {
library.destroy( region, WL_REGION_DESTROY );
return false;
}
library.request( badge, WL_SURFACE_SET_INPUT_REGION, region );
library.destroy( region, WL_REGION_DESTROY );
library.request( badgeRole, WL_SUBSURFACE_SET_POSITION, width / bufferScale - 8,
height / bufferScale - 8 );
library.request( badge, WL_SURFACE_SET_BUFFER_SCALE, bufferScale );
static constexpr wl_buffer_listener bufferListener{ []( void*, wl_buffer* ) {} };
library.listen( buffer, bufferListener, nullptr );
for ( auto* feedbackBuffer : feedbackBuffers )
library.listen( feedbackBuffer, bufferListener, nullptr );
// Each badge has an immutable buffer, so changing feedback never overwrites pixels the
// compositor may still be reading and never copies the main snapshot.
feedback = DragFeedback::Rejected;
library.request( badge, WL_SURFACE_ATTACH, feedbackBuffers[static_cast<size_t>( feedback )], 0,
0 );
library.request( badge, WL_SURFACE_DAMAGE, 0, 0, badgeSize, badgeSize );
library.request( badge, WL_SURFACE_COMMIT );
library.request( icon, WL_SURFACE_SET_BUFFER_SCALE, bufferScale );
// Bind compositor version 4 so attach offsets specify the icon hotspot on old compositors too.
library.request( icon, WL_SURFACE_ATTACH, buffer,
static_cast<int>( std::lround( -offer.preview.hotspot.x / scale ) ),
static_cast<int>( std::lround( -offer.preview.hotspot.y / scale ) ) );
library.request( icon, WL_SURFACE_DAMAGE, 0, 0, width, height );
// The caller assigns the drag-icon role with start_drag before committing this buffer.
return true;
}
void WaylandDragPreview::setFeedback( DragFeedback next ) {
if ( !badge || feedback == next )
return;
feedback = next;
library.request( badge, WL_SURFACE_ATTACH, feedbackBuffers[static_cast<size_t>( feedback )], 0,
0 );
library.request( badge, WL_SURFACE_DAMAGE, 0, 0, 16, 16 );
library.request( badge, WL_SURFACE_COMMIT );
// The badge is a synchronized subsurface; its new state applies with the parent commit.
library.request( icon, WL_SURFACE_COMMIT );
}
void WaylandDragPreview::reset() {
library.destroy( badgeRole, WL_SUBSURFACE_DESTROY );
library.destroy( badge, WL_SURFACE_DESTROY );
library.destroy( icon, WL_SURFACE_DESTROY );
library.destroy( buffer, WL_BUFFER_DESTROY );
for ( auto*& feedbackBuffer : feedbackBuffers )
library.destroy( feedbackBuffer, WL_BUFFER_DESTROY );
}
}}} // namespace EE::Window::Private
#endif
@@ -0,0 +1,44 @@
#ifndef EE_WINDOW_WAYLAND_DRAG_PREVIEW_HPP
#define EE_WINDOW_WAYLAND_DRAG_PREVIEW_HPP
#include "waylanddraganddrop.hpp"
#ifdef EE_HAS_WAYLAND_DRAG_SOURCE
#include "dragpreview.hpp"
#include "waylandlibrary.hpp"
namespace EE { namespace Window { namespace Private {
/** A compositor-owned drag-icon role, independent of SDL popup positioning or pointer capture. */
class WaylandDragPreview final : public NativeDragPreview {
public:
explicit WaylandDragPreview( WaylandLibrary& library ) : library( library ) {}
~WaylandDragPreview() override;
bool initialize( wl_compositor* compositor, wl_subcompositor* subcompositor, wl_shm* shm,
const DragOffer& offer, float scale );
wl_surface* surface() const { return icon; }
void move( int, int ) override {}
void updatePosition() override {}
void setFeedback( DragFeedback feedback ) override;
void reset() override;
private:
WaylandLibrary& library;
wl_surface* icon{ nullptr };
wl_surface* badge{ nullptr };
wl_subsurface* badgeRole{ nullptr };
wl_buffer* buffer{ nullptr };
std::array<wl_buffer*, 3> feedbackBuffers{};
DragFeedback feedback{ DragFeedback::Pending };
};
}}} // namespace EE::Window::Private
#endif
#endif
@@ -0,0 +1,176 @@
#ifndef EE_WINDOW_WAYLAND_LIBRARY_HPP
#define EE_WINDOW_WAYLAND_LIBRARY_HPP
#include <eepp/config.hpp>
// Missing or older protocol headers disable the backend without adding a build dependency.
#if ( EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_BSD ) && \
( __has_include( <wayland-client.h> ) || __has_include( <wayland/wayland-client.h> ) )
#if __has_include( <wayland-client.h> )
#include <wayland-client.h>
#else
#include <wayland/wayland-client.h>
#endif
// Generated marshal flags and their dispatch entry point require client headers >= 1.20.
#ifdef WL_MARSHAL_FLAG_DESTROY
#define EE_HAS_WAYLAND_DRAG_SOURCE
#include <algorithm>
#include <dlfcn.h>
namespace EE { namespace Window { namespace Private {
/** Dynamic core protocol dispatch. Generated Wayland inline functions otherwise introduce direct
* references to libwayland-client's functions and interface descriptors. */
class WaylandLibrary {
public:
WaylandLibrary() = default;
WaylandLibrary( const WaylandLibrary& ) = delete;
WaylandLibrary& operator=( const WaylandLibrary& ) = delete;
~WaylandLibrary() {
if ( handle )
dlclose( handle );
}
bool load() {
if ( handle )
return true;
handle = dlopen( "libwayland-client.so.0", RTLD_NOW | RTLD_LOCAL );
if ( !handle )
handle = dlopen( "libwayland-client.so", RTLD_NOW | RTLD_LOCAL );
if ( !handle )
return false;
#define EE_WL_FUNCTION( name ) \
name = reinterpret_cast<decltype( name )>( dlsym( handle, #name ) ); \
if ( !name ) { \
dlclose( handle ); \
handle = nullptr; \
return false; \
}
EE_WL_FUNCTION( wl_proxy_marshal_flags )
EE_WL_FUNCTION( wl_proxy_add_listener )
EE_WL_FUNCTION( wl_proxy_get_version )
EE_WL_FUNCTION( wl_proxy_destroy )
EE_WL_FUNCTION( wl_proxy_set_queue )
EE_WL_FUNCTION( wl_proxy_create_wrapper )
EE_WL_FUNCTION( wl_proxy_wrapper_destroy )
EE_WL_FUNCTION( wl_display_roundtrip )
EE_WL_FUNCTION( wl_display_flush )
EE_WL_FUNCTION( wl_display_get_error )
EE_WL_FUNCTION( wl_display_create_queue )
EE_WL_FUNCTION( wl_event_queue_destroy )
EE_WL_FUNCTION( wl_display_dispatch_queue_pending )
EE_WL_FUNCTION( wl_display_prepare_read_queue )
EE_WL_FUNCTION( wl_display_read_events )
EE_WL_FUNCTION( wl_display_cancel_read )
EE_WL_FUNCTION( wl_display_get_fd )
#undef EE_WL_FUNCTION
#define EE_WL_INTERFACE( name ) \
name = static_cast<const wl_interface*>( dlsym( handle, "wl_" #name "_interface" ) ); \
if ( !name ) { \
dlclose( handle ); \
handle = nullptr; \
return false; \
}
EE_WL_INTERFACE( registry )
EE_WL_INTERFACE( callback )
EE_WL_INTERFACE( compositor )
EE_WL_INTERFACE( subcompositor )
EE_WL_INTERFACE( subsurface )
EE_WL_INTERFACE( region )
EE_WL_INTERFACE( surface )
EE_WL_INTERFACE( shm )
EE_WL_INTERFACE( shm_pool )
EE_WL_INTERFACE( buffer )
EE_WL_INTERFACE( seat )
EE_WL_INTERFACE( pointer )
EE_WL_INTERFACE( data_device_manager )
EE_WL_INTERFACE( data_device )
EE_WL_INTERFACE( data_source )
#undef EE_WL_INTERFACE
return true;
}
template <typename T> uint32_t version( T* object ) const {
return wl_proxy_get_version( reinterpret_cast<wl_proxy*>( object ) );
}
template <typename T, typename... A>
T* create( void* object, uint32_t opcode, const wl_interface* interface, A... args ) const {
auto* proxy = static_cast<wl_proxy*>( object );
return reinterpret_cast<T*>( wl_proxy_marshal_flags(
proxy, opcode, interface,
std::min( version( proxy ), static_cast<uint32_t>( interface->version ) ), 0, nullptr,
args... ) );
}
template <typename T>
T* bind( wl_registry* registry, uint32_t name, const wl_interface* interface,
uint32_t version ) const {
return reinterpret_cast<T*>( wl_proxy_marshal_flags(
reinterpret_cast<wl_proxy*>( registry ), WL_REGISTRY_BIND, interface, version, 0, name,
interface->name, version, nullptr ) );
}
template <typename T, typename... A>
void request( T* object, uint32_t opcode, A... args ) const {
wl_proxy_marshal_flags( reinterpret_cast<wl_proxy*>( object ), opcode, nullptr,
version( object ), 0, args... );
}
template <typename T> void destroy( T*& object, uint32_t opcode ) const {
if ( object ) {
wl_proxy_marshal_flags( reinterpret_cast<wl_proxy*>( object ), opcode, nullptr,
version( object ), WL_MARSHAL_FLAG_DESTROY );
object = nullptr;
}
}
template <typename T> void releaseProxy( T*& object ) const {
if ( object ) {
wl_proxy_destroy( reinterpret_cast<wl_proxy*>( object ) );
object = nullptr;
}
}
template <typename T, typename L>
bool listen( T* object, const L& listener, void* data ) const {
return wl_proxy_add_listener(
reinterpret_cast<wl_proxy*>( object ),
reinterpret_cast<void ( ** )( void )>( const_cast<L*>( &listener ) ),
data ) == 0;
}
decltype( &::wl_proxy_marshal_flags ) wl_proxy_marshal_flags{ nullptr };
decltype( &::wl_proxy_add_listener ) wl_proxy_add_listener{ nullptr };
decltype( &::wl_proxy_get_version ) wl_proxy_get_version{ nullptr };
decltype( &::wl_proxy_destroy ) wl_proxy_destroy{ nullptr };
decltype( &::wl_proxy_set_queue ) wl_proxy_set_queue{ nullptr };
decltype( &::wl_proxy_create_wrapper ) wl_proxy_create_wrapper{ nullptr };
decltype( &::wl_proxy_wrapper_destroy ) wl_proxy_wrapper_destroy{ nullptr };
decltype( &::wl_display_roundtrip ) wl_display_roundtrip{ nullptr };
decltype( &::wl_display_flush ) wl_display_flush{ nullptr };
decltype( &::wl_display_get_error ) wl_display_get_error{ nullptr };
decltype( &::wl_display_create_queue ) wl_display_create_queue{ nullptr };
decltype( &::wl_event_queue_destroy ) wl_event_queue_destroy{ nullptr };
decltype( &::wl_display_dispatch_queue_pending ) wl_display_dispatch_queue_pending{ nullptr };
decltype( &::wl_display_prepare_read_queue ) wl_display_prepare_read_queue{ nullptr };
decltype( &::wl_display_read_events ) wl_display_read_events{ nullptr };
decltype( &::wl_display_cancel_read ) wl_display_cancel_read{ nullptr };
decltype( &::wl_display_get_fd ) wl_display_get_fd{ nullptr };
const wl_interface *registry{}, *callback{}, *compositor{}, *subcompositor{}, *subsurface{},
*region{}, *surface{}, *shm{}, *shm_pool{}, *buffer{}, *seat{}, *pointer{},
*data_device_manager{}, *data_device{}, *data_source{};
private:
void* handle{ nullptr };
};
}}} // namespace EE::Window::Private
#endif
#endif
#endif
+5 -1
View File
@@ -40,9 +40,13 @@ bool Input::acquireNativeDragCapture() {
void Input::restoreNativeDragCapture() {}
void Input::resetNativeDragInput() {
suppressNativeDragRelease();
restoreNativeDragCapture();
}
void Input::suppressNativeDragRelease() {
resetButton( EE_BUTTON_LEFT );
mNativeDragReleasePending = true;
restoreNativeDragCapture();
}
bool Input::consumeNativeDragRelease() {
+10
View File
@@ -9,6 +9,9 @@
using namespace EE::System;
int runAutomatedNativeDragTests();
#if EE_PLATFORM == EE_PLATFORM_LINUX
int runAutomatedWaylandDragTests( bool receiver, bool processExit = false );
#endif
namespace {
std::string utf8Path( const std::filesystem::path& path ) {
@@ -63,6 +66,13 @@ struct SampleFiles {
EE_MAIN_FUNC int main( int argc, char** argv ) {
auto arguments = Sys::parseArguments( argc, argv );
#if EE_PLATFORM == EE_PLATFORM_LINUX
if ( arguments.size() == 1 && arguments[0] == "--automated-wayland-exit" )
return runAutomatedWaylandDragTests( false, true );
if ( arguments.size() == 1 &&
( arguments[0] == "--automated-wayland" || arguments[0] == "--automated-wayland-target" ) )
return runAutomatedWaylandDragTests( arguments[0] == "--automated-wayland-target" );
#endif
if ( arguments.size() == 1 && arguments[0] == "--automated" ) {
std::cout << "Automated integration checks: synthetic pointer and keyboard input.\n"
"Run on an isolated display.\n"
+358
View File
@@ -0,0 +1,358 @@
#include <array>
#include <chrono>
#include <cstdlib>
#include <eepp/graphics/image.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
#include <eepp/window.hpp>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <poll.h>
#include <thread>
#include <unistd.h>
using namespace EE;
using namespace EE::Window;
using namespace EE::System;
namespace {
using TestClock = std::chrono::steady_clock;
const RGB sourceColor( 180, 30, 50 ), targetColor( 30, 80, 190 ), externalColor( 135, 45, 185 );
enum Scenario : size_t {
Snapshot,
Large,
Rejected,
Cancel,
Escape,
Destroy,
External,
Instant,
StalledCancel,
Shutdown
};
constexpr const char* scenarios[]{ "snapshot", "large", "rejected", "cancel",
"escape", "destroy", "external", "instant",
"stalled-cancel", "shutdown" };
/** Weston controls placement. The isolated runner finds these solid colors on its X11 output;
* no window positioning, pointer warping, or synthetic events occur in the normal manual demo. */
struct WaylandChecks {
EE::Window::Window* source{};
EE::Window::Window* target{};
std::filesystem::path fixtureDirectory;
DragOffer offer;
SmallVector<std::string, 2> received;
std::array<Uint8, 64 * 40 * 4> pixels;
Graphics::Image image{ pixels.data(), 64, 40, 4 };
Graphics::Image rgbImage{ pixels.data(), 64, 40, 3 };
Vector2i press;
TestClock::time_point deadline;
size_t scenario{}, completions{};
size_t clickPresses{}, clickReleases{};
bool armed{}, done{}, reported{}, passed{ true }, cancelPending{}, destroyPending{},
clickPending{}, exitingWithBackend{};
WaylandChecks() {
image.avoidFreeImage( true );
rgbImage.avoidFreeImage( true );
image.fillWithColor( Color( 40, 220, 100 ) );
}
~WaylandChecks() {
Engine::destroySingleton();
removeFixtures();
}
void removeFixtures() {
if ( !fixtureDirectory.empty() ) {
std::error_code error;
for ( const char* name : { "sample #\xc3\xa9-one.txt", "sample two.txt" } )
std::filesystem::remove( fixtureDirectory / name, error );
std::filesystem::remove( fixtureDirectory, error );
fixtureDirectory.clear();
}
}
bool initialize() {
auto candidate = std::filesystem::temp_directory_path() /
( "eepp-wayland-dnd-" + std::to_string( Sys::getProcessID() ) + "-" +
std::to_string( TestClock::now().time_since_epoch().count() ) );
if ( !std::filesystem::create_directory( candidate ) )
return false;
fixtureDirectory = std::move( candidate );
for ( const char* name : { "sample #\xc3\xa9-one.txt", "sample two.txt" } ) {
std::ofstream file( fixtureDirectory / name );
file << "eepp native drag fixture\n";
if ( !file )
return false;
}
target = Engine::instance()->createWindow(
{ 320, 220, "eepp Wayland test target", WindowStyle::Borderless } );
if ( !target || !target->isOpen() )
return false;
// The post-drag click must reach the application even when it also activates the source.
Engine::instance()->getDisplayManager()->enableMouseFocusClickThrough();
target->setClearColor( targetColor );
target->getInput()->pushCallback( [this]( InputEvent* event ) {
if ( event->Type == InputEvent::FileDropped )
received.emplace_back( event->file.file );
} );
return createSource();
}
bool createSource() {
source = Engine::instance()->createWindow(
{ 360, 240, "eepp Wayland test source", WindowStyle::Borderless } );
if ( !source || !source->isOpen() )
return false;
source->setClearColor( sourceColor );
source->getInput()->pushCallback( [this]( InputEvent* event ) {
if ( reported && clickPending ) {
if ( event->Type == InputEvent::MouseButtonDown &&
event->button.button == EE_BUTTON_LEFT )
++clickPresses;
if ( event->Type == InputEvent::MouseButtonUp &&
event->button.button == EE_BUTTON_LEFT )
++clickReleases;
return;
}
if ( done || reported )
return;
if ( event->Type == InputEvent::MouseButtonUp &&
event->button.button == EE_BUTTON_LEFT &&
DragAndDrop::forWindow( *source ).isDragging() ) {
passed = false;
std::cerr << "Native drag delivered a synthetic button-up to the application\n";
}
if ( event->Type == InputEvent::MouseButtonDown &&
event->button.button == EE_BUTTON_LEFT ) {
press = { event->button.x, event->button.y };
armed = true;
} else if ( event->Type == InputEvent::MouseButtonUp ) {
armed = false;
} else if ( event->Type == InputEvent::MouseMotion && armed ) {
const Vector2i delta = Vector2i( event->motion.x, event->motion.y ) - press;
if ( delta.x * delta.x + delta.y * delta.y >= 64 ) {
armed = false;
start();
}
}
} );
return true;
}
void prepare() {
if ( !source && !createSource() ) {
passed = false;
return;
}
offer.files.clear();
const size_t count = scenario == Large ? 4096 : 2;
for ( size_t i = 0; i < count; ++i ) {
offer.files.push_back(
{ ( fixtureDirectory / ( i % 2 ? "sample two.txt" : "sample #\xc3\xa9-one.txt" ) )
.string() } );
}
if ( scenario == External )
rgbImage.fillWithColor( Color( 40, 220, 100 ) );
offer.preview.image = scenario == Snapshot ? &image
: scenario == External ? &rgbImage
: nullptr;
offer.preview.hotspot = { 8, 8 }; // The icon must never intercept target hit-testing.
received.clear();
completions = 0;
armed = done = reported = false;
deadline = TestClock::now() + std::chrono::seconds( 20 );
if ( scenario == Snapshot ) {
const auto* snapshot = offer.preview.image;
std::array<Uint8, 2> monochrome{};
for ( unsigned channels : { 1u, 2u } ) {
Graphics::Image invalidImage( monochrome.data(), 1, 1, channels );
invalidImage.avoidFreeImage( true );
offer.preview.image = &invalidImage;
passed &= DragAndDrop::forWindow( *source ).startDrag( offer ).status ==
DragStartStatus::InvalidOffer;
}
offer.preview.image = snapshot;
}
const auto invalid = DragAndDrop::forWindow( *source ).startDrag( offer );
passed &= invalid.status == DragStartStatus::InvalidGesture;
std::cout << "WAYLAND_DND_READY " << scenarios[scenario] << std::endl;
}
bool waitForRunner() {
pollfd command{ STDIN_FILENO, POLLIN, 0 };
char gate[16];
return poll( &command, 1, 5000 ) > 0 && read( STDIN_FILENO, gate, sizeof( gate ) ) > 0;
}
void start() {
if ( scenario == StalledCancel ) {
std::cout << "WAYLAND_DND_BEFORE_START" << std::endl;
passed &= waitForRunner();
}
auto drag = DragAndDrop::forWindow( *source );
const auto result = drag.startDrag( offer, [this]( DragResult result ) {
if ( exitingWithBackend )
std::_Exit( 1 ); // Process teardown must never deliver the completion callback.
++completions;
const bool copied = scenario == Snapshot || scenario == Large || scenario == External;
passed &=
result.reason == ( copied ? DragEndReason::Dropped : DragEndReason::Cancelled );
passed &= result.performedAction == ( copied ? DragAction::Copy : DragAction::None );
if ( source ) {
passed &= !source->getInput()->isMouseLeftPressed();
passed &= !( source->getInput()->getClickTrigger() & EE_BUTTON_LMASK );
passed &= !( source->getInput()->getDoubleClickTrigger() & EE_BUTTON_LMASK );
passed &= !DragAndDrop::forWindow( *source ).isDragging();
}
done = true;
} );
passed &= result.status == DragStartStatus::Started;
if ( result.status != DragStartStatus::Started ) {
std::cerr << "Wayland start failed: " << result.error << '\n';
done = true;
}
passed &= drag.startDrag( offer ).status == DragStartStatus::Busy;
if ( scenario == Instant ) {
drag.cancelDrag();
passed &= completions == 0;
}
cancelPending = scenario == Cancel || scenario == StalledCancel;
destroyPending = scenario == Destroy;
std::cout << "WAYLAND_DND_STARTED " << scenarios[scenario] << std::endl;
// Weston was stopped before startDrag(), so no setup acknowledgement can have arrived.
// Avoid rendering or pumping SDL until the runner requests cancellation.
if ( scenario == StalledCancel )
passed &= waitForRunner();
}
void draw() {
for ( auto* window : { source, target } ) {
if ( window && window->isOpen() ) {
auto current = Engine::instance()->makeWindowCurrent( window );
window->setClearColor( window == source ? sourceColor : targetColor );
window->clear();
window->display();
}
}
}
int run( bool processExit = false ) {
exitingWithBackend = processExit;
if ( !initialize() )
return 1;
if ( !DragAndDrop::forWindow( *source ).getCapabilities().fileSource )
return 77;
if ( processExit )
scenario = Shutdown;
prepare();
while ( scenario < std::size( scenarios ) && passed ) {
Engine::instance()->updateInput();
if ( source && DragAndDrop::forWindow( *source ).isDragging() ) {
passed &= !( source->getInput()->getClickTrigger() & EE_BUTTON_LMASK );
passed &= !( source->getInput()->getDoubleClickTrigger() & EE_BUTTON_LMASK );
}
if ( clickPending && clickReleases ) {
passed &= clickPresses == 1 && clickReleases == 1;
passed &= ( source->getInput()->getClickTrigger() & EE_BUTTON_LMASK ) != 0;
clickPending = false;
std::cout << "WAYLAND_DND_CLICK " << ( passed ? "PASS" : "FAIL" ) << std::endl;
}
if ( scenario == Shutdown && DragAndDrop::forWindow( *source ).isDragging() ) {
if ( processExit ) {
// std::exit deliberately skips stack destructors and leaves Engine/windows
// alive. Its registered handlers must release the native session without a
// completion.
removeFixtures();
std::cout << "WAYLAND_DND_PROCESS_EXIT" << std::endl;
std::exit( passed ? 0 : 1 );
}
Engine::destroySingleton();
source = target = nullptr;
passed &= completions == 0;
std::cout << "WAYLAND_DND_RESULT shutdown " << ( passed ? "PASS" : "FAIL" )
<< " completions=" << completions << std::endl;
return passed ? 0 : 1;
}
if ( cancelPending ) {
cancelPending = false;
DragAndDrop::forWindow( *source ).cancelDrag();
if ( scenario == StalledCancel )
std::cout << "WAYLAND_DND_CLEANUP_DONE" << std::endl;
}
if ( destroyPending ) {
destroyPending = false;
auto* destroyed = source;
source = nullptr;
Engine::instance()->destroyWindow( destroyed );
}
if ( done && !reported ) {
passed &= completions == 1;
const size_t expected =
( scenario == Snapshot || scenario == Large ) ? offer.files.size() : 0;
passed &= received.size() == expected;
for ( size_t i = 0; i < std::min( received.size(), expected ); ++i )
passed &= received[i] == offer.files[i].path;
reported = true;
std::cout << "WAYLAND_DND_RESULT " << scenarios[scenario] << ' '
<< ( passed ? "PASS" : "FAIL" ) << " files=" << received.size()
<< " completions=" << completions << std::endl;
}
pollfd input{ STDIN_FILENO, POLLIN, 0 };
if ( reported && poll( &input, 1, 0 ) > 0 && ( input.revents & POLLIN ) ) {
char command[16];
if ( read( STDIN_FILENO, command, sizeof( command ) ) <= 0 )
return 1;
if ( command[0] == 'c' ) {
clickPending = true;
clickPresses = clickReleases = 0;
std::cout << "WAYLAND_DND_CLICK_READY" << std::endl;
continue;
}
++scenario;
if ( scenario < std::size( scenarios ) )
prepare();
}
if ( TestClock::now() > deadline ) {
std::cerr << "Wayland scenario timed out\n";
return 1;
}
draw();
std::this_thread::sleep_for( std::chrono::milliseconds( 5 ) );
}
return passed ? 0 : 1;
}
};
} // namespace
int runAutomatedWaylandDragTests( bool receiver, bool processExit ) {
if ( !receiver ) {
WaylandChecks checks;
return checks.run( processExit );
}
auto* window = Engine::instance()->createWindow(
{ 320, 220, "eepp Wayland external target", WindowStyle::Borderless } );
if ( !window || !window->isOpen() )
return 1;
window->setClearColor( externalColor );
SmallVector<std::string, 2> received;
window->getInput()->pushCallback( [&]( InputEvent* event ) {
if ( event->Type == InputEvent::FileDropped )
received.emplace_back( FileSystem::fileNameFromPath( event->file.file ) );
} );
std::cout << "WAYLAND_DND_EXTERNAL_READY" << std::endl;
const auto deadline = TestClock::now() + std::chrono::seconds( 20 );
while ( received.size() < 2 && TestClock::now() < deadline ) {
Engine::instance()->updateInput();
window->clear();
window->display();
std::this_thread::sleep_for( std::chrono::milliseconds( 5 ) );
}
const bool passed =
received == SmallVector<std::string, 2>{ "sample #\xc3\xa9-one.txt", "sample two.txt" };
Engine::destroySingleton();
std::cout << "WAYLAND_DND_EXTERNAL " << ( passed ? "PASS" : "FAIL" ) << std::endl;
return passed ? 0 : 1;
}
+7 -1
View File
@@ -142,8 +142,14 @@ int runAutomatedNativeDragTests() {
window->display();
std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
auto drag = DragAndDrop::forWindow( *window );
if ( !drag.getCapabilities().fileSource )
if ( Private::NativeDragAccess::getNativeDragContext( *window ).kind !=
NativeWindowContext::Kind::X11 ||
!drag.getCapabilities().fileSource ) {
Engine::destroySingleton();
x11.XCloseDisplay( peer );
dlclose( library );
return 77;
}
auto target =
x11.XCreateSimpleWindow( peer, DefaultRootWindow( peer ), 500, 400, 300, 200, 0, 0, 0 );
x11.XStoreName( peer, target, "eepp XDND integration target" );