mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-27 08:37:58 +03:00
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:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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" }
|
||||
|
||||
Executable
+299
@@ -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
|
||||
|
||||
@@ -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 );
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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" );
|
||||
|
||||
Reference in New Issue
Block a user