diff --git a/.gitignore b/.gitignore index 567609b..3d43339 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ build/ +__pycache__/ diff --git a/DEVELOPER.md b/DEVELOPER.md index 44dd363..2b7c4b6 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -72,6 +72,7 @@ Located in `main.cpp`, registered via `HyprlandAPI::addConfigValueV2`: - `plugin:hyprview:window_name_bg_opacity` - `plugin:hyprview:window_text_color` - `plugin:hyprview:gesture_distance` +- `plugin:hyprview:fullscreen_on_select` (default 0) ### Framebuffer Management - Individual framebuffers per window stored in `SWindowImage::fb` diff --git a/README.md b/README.md index 601da18..ebbf526 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ https://github.com/user-attachments/assets/c0553bfe-6357-48e5-a4d0-50068096d800 ## Features +* **Fullscreen Windows:** Include fullscreen windows and restore their original fullscreen modes and workspaces when dismissing the overview. +* **Optional Fullscreen Selection:** Enable `fullscreen_on_select` to make an explicitly selected window fullscreen. * **Workspace Overview:** See all your open windows on the current workspace at a glance. * **Multi-Workspace Modes:** View windows from the current workspace, all workspaces on the monitor, or include special (scratchpad) workspaces. * **Workspace Indicator:** Each window tile shows its workspace ID (displayed as "wsid:N") in a configurable position with customizable size and styling. The indicator color automatically matches the window's border color (active or inactive) for easy navigation across multiple workspaces. @@ -77,6 +79,28 @@ so a downward callback can close it. Avoid assigning competing workspace gesture to the same fingers/direction. Legacy `hyprview-gesture` remains available in `hyprland.conf`. +### Fullscreen selection + +By default, selecting a preview retains its previous fullscreen state. The +existing sticky `on` mode also retains its behavior of focusing a preview without +closing. To make a left-click or explicit selection close the overview and make +the chosen window fullscreen, opt in: + +```lua +-- Apply after the plugin is loaded (or in the config reload following its load). +hl.config({ plugin = { hyprview = { fullscreen_on_select = 1 } } }) +``` + +For legacy configuration: + +```ini +plugin:hyprview:fullscreen_on_select = 1 +``` + +The default is `0`. A downward gesture or `off` without a selection restores the +original window states regardless of this setting. With the option enabled, +clicking a preview also exits sticky `on` mode. Empty background clicks are ignored. + ### Keybinds You can bind the overview to a key. The dispatcher accepts optional arguments to control the behavior. diff --git a/src/hyprview.cpp b/src/hyprview.cpp index c521b4b..fe11c9a 100644 --- a/src/hyprview.cpp +++ b/src/hyprview.cpp @@ -1,3 +1,4 @@ +#include #include "hyprview.hpp" #include #include @@ -71,6 +72,12 @@ CHyprView::~CHyprView() { window->moveToWorkspace(image.originalWorkspace); } } + for (const auto &image : images) { + auto window = image.pWindow.lock(); + if (window && window->m_isMapped && image.originalFullscreen.internal != Fullscreen::FSMODE_NONE) + Fullscreen::controller()->setFullscreenMode(window, image.originalFullscreen.internal, + image.originalFullscreen.client); + } } // Always cleanup resources in destructor if they haven't been cleaned yet @@ -80,7 +87,9 @@ CHyprView::~CHyprView() { images.clear(); if (bgFramebuffer) bgFramebuffer->release(); - g_pPointerManager->resetCursorImage(); + // resetCursorImage() clears the buffer without invalidating the renderer's + // cached shape, so later requests for that same shape can leave it invisible. + g_pHyprRenderer->setCursorFromName("left_ptr", true); } } @@ -91,8 +100,17 @@ void CHyprView::setupWindowImages(std::vector &windowsToRender) { // Save original workspaces BEFORE moving std::unordered_map originalWorkspaces; + std::unordered_map originalFullscreen; for (auto &window : windowsToRender) { originalWorkspaces[window] = window->m_workspace; + originalFullscreen[window] = Fullscreen::controller()->getFullscreenModes(window); + } + + // The overview owns rendering temporarily. Remove covering fullscreen state + // before combining windows from different workspaces, keeping client state. + for (auto &window : windowsToRender) { + if (originalFullscreen[window].internal != Fullscreen::FSMODE_NONE) + Fullscreen::controller()->setFullscreenMode(window, Fullscreen::FSMODE_NONE); } // Move windows to active workspace so they have valid surfaces for rendering @@ -115,6 +133,7 @@ void CHyprView::setupWindowImages(std::vector &windowsToRender) { image.originalPos = window->positionAnimation()->value(); image.originalSize = window->sizeAnimation()->value(); image.originalWorkspace = originalWorkspaces[window]; + image.originalFullscreen = originalFullscreen[window]; const auto RENDERSIZE = (window->sizeAnimation()->value() * pMonitor->m_scale).floor(); @@ -348,10 +367,6 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, if (!w->m_isMapped || w->isHidden()) continue; - // Skip fullscreen windows to prevent problems and crashes - if (Fullscreen::controller()->isFullscreen(w)) - continue; - if (!shouldIncludeWindow(w)) continue; @@ -451,7 +466,7 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, g_pHyprRenderer->m_bBlockSurfaceFeedback = false; - g_pCursorManager->setCursorFromName("left_ptr"); + g_pHyprRenderer->setCursorFromName("left_ptr", true); lastMousePosLocal = g_pInputManager->getMouseCoordsInternal() - pMonitor->m_position; @@ -463,7 +478,7 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, // Check if mouse is actually on this monitor BEFORE cancelling Vector2D globalMousePos = g_pInputManager->getMouseCoordsInternal(); Vector2D monitorPos = pMonitor->m_position; - Vector2D fullMonitorSize = pMonitor->m_pixelSize; + Vector2D fullMonitorSize = pMonitor->m_pixelSize / pMonitor->m_scale; bool mouseOnThisMonitor = (globalMousePos.x >= monitorPos.x && @@ -493,7 +508,7 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, // Check if mouse is on this monitor BEFORE cancelling Vector2D globalMousePos = g_pInputManager->getMouseCoordsInternal(); Vector2D monitorPos = pMonitor->m_position; - Vector2D fullMonitorSize = pMonitor->m_pixelSize; + Vector2D fullMonitorSize = pMonitor->m_pixelSize / pMonitor->m_scale; bool mouseOnThisMonitor = (globalMousePos.x >= monitorPos.x && @@ -505,8 +520,10 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, return; // Mouse is on a different monitor - don't cancel event } + static const CConfigValue PFULLSCREENONSELECT( + "plugin:hyprview:fullscreen_on_select"); // If explicitly turned on, project click to real window - if (stickyOn) { + if (stickyOn && !*PFULLSCREENONSELECT) { info.cancelled = true; Vector2D localMousePos = globalMousePos - monitorPos; @@ -520,8 +537,8 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, // Calculate mouse position relative to tile const CBox &tileBox = images[tileIndex].box; - Vector2D mousePosInTile = {localMousePos.x - tileBox.x, - localMousePos.y - tileBox.y}; + Vector2D mousePosInTile = {localMousePos.x * pMonitor->m_scale - tileBox.x, + localMousePos.y * pMonitor->m_scale - tileBox.y}; // Calculate scale factor from tile to real window Vector2D realWindowSize = window->sizeAnimation()->value(); @@ -544,17 +561,24 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, return; } - // Normal mode: cancel click, select window, and close ALL overviews except - // forced ones info.cancelled = true; - selectHoveredWindow(); + lastMousePosLocal = globalMousePos - monitorPos; + const int tileIndex = getWindowIndexFromMousePos(lastMousePosLocal); + if (tileIndex < 0 || tileIndex >= (int)images.size()) + return; + auto selectedWindow = images[tileIndex].pWindow.lock(); + if (!selectedWindow || !selectedWindow->m_isMapped) + return; - // Close all overview instances except those with stickyOn=true + currentHoveredIndex = tileIndex; + selectHoveredWindow(); + // Restore other monitors first, then focus this selection. for (auto &[monitor, instance] : g_pHyprViewInstances) { - if (instance && !instance->stickyOn) { + if (instance && instance.get() != this && + (!instance->stickyOn || *PFULLSCREENONSELECT)) instance->close(); - } } + close(); }; auto onMouseAxis = [this](SCallbackInfo &info) { @@ -564,7 +588,7 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, // Check if mouse is on this monitor Vector2D globalMousePos = g_pInputManager->getMouseCoordsInternal(); Vector2D monitorPos = pMonitor->m_position; - Vector2D fullMonitorSize = pMonitor->m_pixelSize; + Vector2D fullMonitorSize = pMonitor->m_pixelSize / pMonitor->m_scale; bool mouseOnThisMonitor = (globalMousePos.x >= monitorPos.x && @@ -601,7 +625,10 @@ CHyprView::CHyprView(PHLMONITOR pMonitor_, PHLWORKSPACE startedOn_, bool swipe_, auto& EV = Event::bus()->m_events; mouseMoveHook = EV.input.mouse.move.listen([onCursorMove](const Vector2D&, SCallbackInfo& info) { onCursorMove(info); }); touchMoveHook = EV.input.touch.motion.listen([onCursorMove](const ITouch::SMotionEvent&, SCallbackInfo& info) { onCursorMove(info); }); - mouseButtonHook = EV.input.mouse.button.listen([onCursorSelect](const IPointer::SButtonEvent&, SCallbackInfo& info) { onCursorSelect(info); }); + mouseButtonHook = EV.input.mouse.button.listen([onCursorSelect](const IPointer::SButtonEvent& event, SCallbackInfo& info) { + if (event.button == BTN_LEFT && event.state == WL_POINTER_BUTTON_STATE_PRESSED) + onCursorSelect(info); + }); mouseAxisHook = EV.input.mouse.axis.listen([onMouseAxis](const IPointer::SAxisEvent&, SCallbackInfo& info) { onMouseAxis(info); }); touchDownHook = EV.input.touch.down.listen([onCursorSelect](const ITouch::SDownEvent&, SCallbackInfo& info) { onCursorSelect(info); }); @@ -768,6 +795,14 @@ void CHyprView::close() { } } + // Restore fullscreen only after all windows are back on their workspaces. + for (const auto &image : images) { + auto window = image.pWindow.lock(); + if (window && window->m_isMapped && image.originalFullscreen.internal != Fullscreen::FSMODE_NONE) + Fullscreen::controller()->setFullscreenMode(window, image.originalFullscreen.internal, + image.originalFullscreen.client); + } + // STEP 2: Start closing animationi - animate scale back to 0 Debug::log(LOG, "[hyprview] close(): Start closing animation"); *scale = 0.0f; @@ -776,6 +811,11 @@ void CHyprView::close() { if (userExplicitlySelected && selectedWindow) { Desktop::focusState()->fullWindowFocus(selectedWindow, Desktop::FOCUS_REASON_KEYBIND); Config::Actions::alterZOrder("top"); + static const CConfigValue PFULLSCREENONSELECT( + "plugin:hyprview:fullscreen_on_select"); + if (*PFULLSCREENONSELECT) + Fullscreen::controller()->setFullscreenMode(selectedWindow, Fullscreen::FSMODE_FULLSCREEN, + Fullscreen::FSMODE_FULLSCREEN); } } @@ -792,7 +832,9 @@ void CHyprView::onPreRender() { images.clear(); if (bgFramebuffer) bgFramebuffer->release(); - g_pPointerManager->resetCursorImage(); + // resetCursorImage() clears the buffer without invalidating the renderer's + // cached shape, so later requests for that same shape can leave it invisible. + g_pHyprRenderer->setCursorFromName("left_ptr", true); } } @@ -1213,7 +1255,8 @@ void CHyprView::onSwipeEnd() { m_isSwiping = false; } -int CHyprView::getWindowIndexFromMousePos(const Vector2D &mousePos) { +int CHyprView::getWindowIndexFromMousePos(const Vector2D &logicalMousePos) { + const Vector2D mousePos = logicalMousePos * pMonitor->m_scale; if (images.empty()) return -1; diff --git a/src/hyprview.hpp b/src/hyprview.hpp index 2ce150c..8125b5c 100644 --- a/src/hyprview.hpp +++ b/src/hyprview.hpp @@ -111,6 +111,7 @@ class CHyprView { struct SWindowImage { SP fb; PHLWINDOWREF pWindow; + Fullscreen::SFullscreenMode originalFullscreen; CBox box; Vector2D originalPos; Vector2D originalSize; diff --git a/src/main.cpp b/src/main.cpp index 5c660f1..c37aa3e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -844,6 +844,10 @@ APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:window_name_bg_opacity"); if (!HyprlandAPI::addConfigValueV2(PHANDLE, makeShared("plugin:hyprview:window_text_color", "Hyprview option", 0xFFFFFFFF))) throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:window_text_color"); + if (!HyprlandAPI::addConfigValueV2(PHANDLE, + makeShared("plugin:hyprview:fullscreen_on_select", + "Fullscreen an explicitly selected window", 0))) + throw std::runtime_error("[hyprview] Cannot register plugin:hyprview:fullscreen_on_select"); HyprlandAPI::reloadConfig(); return {"hyprview", "Window overview with multiple placement algorithms", diff --git a/tests/README.md b/tests/README.md index 3050473..6f9cfdd 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,6 +7,8 @@ Python 3, Hyprland/hyprctl, foot, and grim, plus a running Wayland compositor. make -C src python3 tests/nested.py build/hyprview.so --scale 1 python3 tests/nested.py build/hyprview.so --scale 2 +python3 tests/nested.py build/hyprview.so --scale 1 --fullscreen --selection default +python3 tests/nested.py build/hyprview.so --scale 2 --fullscreen --selection fullscreen ``` Use `--parent-display wayland-N` if the terminal's `WAYLAND_DISPLAY` is stale. @@ -19,3 +21,23 @@ Checks cover two windows on separate workspaces, three open/close cycles, exact workspace/fullscreen-state restoration, config reload, and unloading while open. This exercises Lua callbacks directly; physical touchpad gestures still require a manual check. It does not cover older Hyprland versions or rotated outputs. + +`--fullscreen` adds a fullscreen window and checks that the overview temporarily +clears its internal fullscreen mode, then restores both internal and client modes. +`--selection` additionally requires a C compiler, pkg-config, wayland-client +development files, and wayland-scanner. It builds a virtual pointer helper and +checks empty-background clicks, selection across workspaces, the default sticky +behavior, and optional fullscreen selection (including an already-fullscreen +selection). The helper connects only to the test compositor's socket. + +`virtual-pointer.xml` comes from swaywm/wlr-protocols, at +`unstable/wlr-virtual-pointer-unstable-v1.xml`; its upstream license notice is +retained in the file. + +Use `--check-cursor` to verify that a cursor image remains after dismissal, +selection, and unloading. This builds a small read-only observer plugin against +the installed Hyprland headers and loads it only into the nested compositor. +It requires a C++ compiler and `pkg-config --cflags hyprland` to resolve matching +headers (set `PKG_CONFIG_PATH` if using hyprpm's header installation). The check +fails on the previous cleanup code, which clears the cursor buffer while leaving +the renderer's cached cursor shape intact. diff --git a/tests/cursor-probe.cpp b/tests/cursor-probe.cpp new file mode 100644 index 0000000..ddffe08 --- /dev/null +++ b/tests/cursor-probe.cpp @@ -0,0 +1,19 @@ +#include +#include +#include +#include + +// Test-only observer. Load exclusively in the nested test compositor. +APICALL EXPORT std::string PLUGIN_API_VERSION() { return HYPRLAND_API_VERSION; } + +APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { + if (!HyprlandAPI::addLuaFunction(handle, "hyprview_test", "has_cursor", [](lua_State *L) -> int { + const auto &cursor = Pointer::mgr()->currentCursorImage(); + lua_pushboolean(L, cursor.pBuffer || cursor.surface); + return 1; + })) + throw std::runtime_error("Cannot register cursor test observer"); + return {"hyprview-cursor-test", "Observe the cursor image in nested tests", "Hyprview tests", "1"}; +} + +APICALL EXPORT void PLUGIN_EXIT() {} diff --git a/tests/nested.py b/tests/nested.py index 1cb7334..cbb2d5a 100644 --- a/tests/nested.py +++ b/tests/nested.py @@ -6,6 +6,7 @@ import os from pathlib import Path import shutil +import shlex import subprocess import tempfile import time @@ -16,6 +17,11 @@ def main(): parser.add_argument("plugin", type=lambda p: Path(p).resolve()) parser.add_argument("--parent-display", default=os.environ.get("WAYLAND_DISPLAY")) parser.add_argument("--scale", type=int, choices=(1, 2), default=1) + parser.add_argument("--fullscreen", action="store_true", help="start with a fullscreen window") + parser.add_argument("--selection", choices=("default", "fullscreen"), + help="also exercise real pointer selection") + parser.add_argument("--check-cursor", action="store_true", + help="build a test observer and check the cursor after closing") args = parser.parse_args() if not args.plugin.is_file() or not args.parent_display: parser.error("provide a built plugin and an existing parent Wayland display") @@ -26,6 +32,21 @@ def main(): # Retain the log and screenshots on failure as well as success. root = Path(tempfile.mkdtemp(prefix="hyprview-test-")) print(f"Test artifacts: {root}", flush=True) + if args.check_cursor: + flags = shlex.split(subprocess.check_output( + ["pkg-config", "--cflags", "hyprland"], text=True)) + subprocess.run(["c++", "-shared", "-fPIC", "-fno-gnu-unique", "-std=c++2b", + *flags, str(Path(__file__).resolve().parent / "cursor-probe.cpp"), + "-o", str(root / "cursor-probe.so")], check=True) + if args.selection: + test_dir = Path(__file__).resolve().parent + for mode, output in (("client-header", "virtual-pointer.h"), ("private-code", "virtual-pointer.c")): + subprocess.run(["wayland-scanner", mode, str(test_dir / "virtual-pointer.xml"), + str(root / output)], check=True) + flags = shlex.split(subprocess.check_output( + ["pkg-config", "--cflags", "--libs", "wayland-client"], text=True)) + subprocess.run(["cc", str(test_dir / "pointer.c"), str(root / "virtual-pointer.c"), + "-I", str(root), "-o", str(root / "pointer"), *flags], check=True) config = root / "hyprland.lua" config.write_text( 'hl.monitor({ output="", mode="1280x800@60", position="1280x0", scale=1 })\n' @@ -52,7 +73,9 @@ def main(): def ctl(*command): assert child.poll() is None, "nested compositor crashed" result = subprocess.run(["hyprctl", "-i", instance["instance"], *command], - capture_output=True, text=True, timeout=10, check=True) + capture_output=True, text=True, timeout=10) + if result.returncode: + raise RuntimeError(f"hyprctl failed: {result.stdout} {result.stderr}") if "error" in result.stdout.lower() or "invalid" in result.stdout.lower(): raise RuntimeError(result.stdout) return result.stdout @@ -71,9 +94,29 @@ def overview(command): lua(f"assert(hl.plugin.hyprview.toggle({json.dumps(command)}))") time.sleep(1.5) + def check_cursor(): + if args.check_cursor: + lua('assert(hl.plugin.hyprview_test.has_cursor(), "cursor image disappeared")') + + def click(x, y): + # The protocol uses logical coordinates over all active outputs. + monitors = json.loads(ctl("-j", "monitors")) + target = next(m for m in monitors if m["name"] == "TEST") + left = min(m["x"] for m in monitors) + top = min(m["y"] for m in monitors) + right = max(m["x"] + m["width"] / m["scale"] for m in monitors) + bottom = max(m["y"] + m["height"] / m["scale"] for m in monitors) + values = (target["x"] + x / target["scale"] - left, + target["y"] + y / target["scale"] - top, right - left, bottom - top) + subprocess.run([str(root / "pointer"), instance["wl_socket"], + *(str(int(v)) for v in values)], check=True, timeout=10) + time.sleep(1) + time.sleep(1) ctl("output", "create", "headless", "TEST") ctl("plugin", "load", str(args.plugin)) + if args.check_cursor: + ctl("plugin", "load", str(root / "cursor-probe.so")) lua('hl.dispatch(hl.dsp.focus({monitor="TEST"}))') lua('hl.exec_cmd("foot --title=Hyprview-test-one"); ' 'hl.exec_cmd("foot --title=Hyprview-test-two")') @@ -84,15 +127,22 @@ def overview(command): other_workspace = clients()[0]["workspace"]["id"] + 1 lua(f'hl.dispatch(hl.dsp.window.move({{workspace="{other_workspace}",follow=false}}))') time.sleep(0.5) + if args.fullscreen: + lua('hl.dispatch(hl.dsp.window.fullscreen({mode="fullscreen",action="set"}))') + time.sleep(0.5) before = state() assert len({s[0] for s in before.values()}) == 2, before for cycle in range(3): overview("on all special") + check_cursor() + if args.fullscreen: + assert all(c["fullscreen"] == 0 for c in clients()), clients() if cycle == 0: subprocess.run(["grim", "-o", "TEST", str(root / "overview.png")], env=dict(env, WAYLAND_DISPLAY=instance["wl_socket"]), check=True, timeout=10) overview("off") + check_cursor() assert state() == before, (before, state()) print(f"Open/close cycle {cycle + 1}: PASS", flush=True) ctl("reload") @@ -100,11 +150,43 @@ def overview(command): assert not ctl("configerrors").strip() overview("on all special") overview("off") + check_cursor() assert state() == before + if args.selection: + if args.selection == "fullscreen": + lua('hl.config({plugin={hyprview={fullscreen_on_select=1}}})') + else: + # The default sticky mode focuses a preview without closing. + overview("on all special") + click(960, 380) + assert len({c["workspace"]["id"] for c in clients()}) == 1 + overview("off") + assert state() == before + + overview("on all special" if args.selection == "fullscreen" else "all special") + click(640, 50) # Empty background must not select a stale hover. + assert len({c["workspace"]["id"] for c in clients()}) == 1 + expected = next(addr for addr, saved in before.items() if saved[0] == other_workspace) + click(960, 380) + check_cursor() + active = json.loads(ctl("-j", "activewindow")) + assert active["address"] == expected, active + assert active["workspace"]["id"] == other_workspace, active + assert active["fullscreen"] == (2 if args.selection == "fullscreen" else before[expected][1]), active + assert {addr: s[0] for addr, s in state().items()} == {addr: s[0] for addr, s in before.items()} + if args.selection == "fullscreen": + overview("on all special") + click(320, 380) + check_cursor() + active = json.loads(ctl("-j", "activewindow")) + assert active["address"] == expected and active["fullscreen"] == 2, active + before = state() + print(f"Pointer selection ({args.selection}): PASS", flush=True) # Unload while open must restore windows as well. overview("on all special") ctl("plugin", "unload", str(args.plugin)) time.sleep(1) + check_cursor() assert state() == before print("Reload, reopen, and unload restoration: PASS", flush=True) finally: diff --git a/tests/pointer.c b/tests/pointer.c new file mode 100644 index 0000000..ee72356 --- /dev/null +++ b/tests/pointer.c @@ -0,0 +1,42 @@ +#include +#include +#include +#include +#include +#include +#include "virtual-pointer.h" +static struct zwlr_virtual_pointer_manager_v1 *manager; +static void global(void *data, struct wl_registry *r, uint32_t name, const char *iface, uint32_t version) { + if (!strcmp(iface, zwlr_virtual_pointer_manager_v1_interface.name)) + manager=wl_registry_bind(r,name,&zwlr_virtual_pointer_manager_v1_interface,1); +} +static void removed(void *data, struct wl_registry *r, uint32_t name) {} +static const struct wl_registry_listener listener={global,removed}; +static uint32_t now_ms(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC,&t); return t.tv_sec*1000+t.tv_nsec/1000000; } +int main(int argc,char **argv) { + if(argc!=6) return 2; + struct wl_display *display=wl_display_connect(argv[1]); + if(!display) return 3; + struct wl_registry *registry=wl_display_get_registry(display); + wl_registry_add_listener(registry,&listener,NULL); + wl_display_roundtrip(display); + if(!manager) return 4; + struct zwlr_virtual_pointer_v1 *pointer=zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager,NULL); + wl_display_roundtrip(display); + zwlr_virtual_pointer_v1_motion_absolute(pointer,now_ms(),atoi(argv[2]),atoi(argv[3]),atoi(argv[4]),atoi(argv[5])); + zwlr_virtual_pointer_v1_frame(pointer); + wl_display_roundtrip(display); + usleep(100000); + zwlr_virtual_pointer_v1_button(pointer,now_ms(),272,WL_POINTER_BUTTON_STATE_PRESSED); + zwlr_virtual_pointer_v1_frame(pointer); + wl_display_roundtrip(display); + usleep(100000); + zwlr_virtual_pointer_v1_button(pointer,now_ms(),272,WL_POINTER_BUTTON_STATE_RELEASED); + zwlr_virtual_pointer_v1_frame(pointer); + wl_display_roundtrip(display); + zwlr_virtual_pointer_v1_destroy(pointer); + zwlr_virtual_pointer_manager_v1_destroy(manager); + wl_registry_destroy(registry); + wl_display_disconnect(display); + return 0; +} diff --git a/tests/virtual-pointer.xml b/tests/virtual-pointer.xml new file mode 100644 index 0000000..ea243e7 --- /dev/null +++ b/tests/virtual-pointer.xml @@ -0,0 +1,152 @@ + + + + Copyright © 2019 Josef Gajdusek + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + This protocol allows clients to emulate a physical pointer device. The + requests are mostly mirror opposites of those specified in wl_pointer. + + + + + + + + + + The pointer has moved by a relative amount to the previous request. + + Values are in the global compositor space. + + + + + + + + + The pointer has moved in an absolute coordinate frame. + + Value of x can range from 0 to x_extent, value of y can range from 0 + to y_extent. + + + + + + + + + + + A button was pressed or released. + + + + + + + + + Scroll and other axis requests. + + + + + + + + + Indicates the set of events that logically belong together. + + + + + + Source information for scroll and other axis. + + + + + + + Stop notification for scroll and other axes. + + + + + + + + Discrete step information for scroll and other axes. + + This event allows the client to extend data normally sent using the axis + event with discrete value. + + + + + + + + + + + + + + + This object allows clients to create individual virtual pointer objects. + + + + + Creates a new virtual pointer. The optional seat is a suggestion to the + compositor. + + + + + + + + + + + + + Creates a new virtual pointer. The seat and the output arguments are + optional. If the seat argument is set, the compositor should assign the + input device to the requested seat. If the output argument is set, the + compositor should map the input device to the requested output. + + + + + + +