diff --git a/.gitmodules b/.gitmodules index e863dde..2690634 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,7 @@ [submodule "hyprland-protocols"] path = subprojects/hyprland-protocols - url = https://github.com/hyprwm/hyprland-protocols + url = https://github.com/3l0w/hyprland-protocols + branch = feat/input-capture-impl [submodule "subprojects/sdbus-cpp"] path = subprojects/sdbus-cpp url = https://github.com/Kistler-Group/sdbus-cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 871ca3c..3d21329 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,8 @@ pkg_check_modules( libpipewire-0.3>=1.1.82 libspa-0.2 libdrm + libeis-1.0 + dbus-1 gbm hyprlang>=0.2.0 hyprutils>=0.2.6 @@ -130,6 +132,8 @@ protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-global-shortcuts-v1" true) protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-toplevel-export-v1" true) +protocolnew("${HYPRLAND_PROTOCOLS}/protocols" "hyprland-input-capture-v1" + true) protocolnew("stable/linux-dmabuf" "linux-dmabuf-v1" false) # Installation diff --git a/hyprland.portal b/hyprland.portal index d3e2cbe..cbaafa8 100644 --- a/hyprland.portal +++ b/hyprland.portal @@ -1,4 +1,4 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.hyprland -Interfaces=org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.GlobalShortcuts; +Interfaces=org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.GlobalShortcuts;org.freedesktop.impl.portal.InputCapture; UseIn=wlroots;Hyprland;sway;Wayfire;river; diff --git a/meson.build b/meson.build index bcae2b2..c2a2675 100644 --- a/meson.build +++ b/meson.build @@ -61,6 +61,9 @@ install_data( 'hyprland.portal', install_dir: join_paths(get_option('datadir'), 'xdg-desktop-portal', 'portals'), ) +version = run_command('cat', files('VERSION'), check: true).stdout().strip() + +add_project_arguments(f'-DXDPH_VERSION="@version@"', language : 'cpp') inc = include_directories('.', 'protocols') diff --git a/protocols/meson.build b/protocols/meson.build index f10d4c8..d7805d1 100644 --- a/protocols/meson.build +++ b/protocols/meson.build @@ -22,6 +22,7 @@ client_protocols = [ 'wlr-foreign-toplevel-management-unstable-v1.xml', hl_protocol_dir / 'protocols/hyprland-toplevel-export-v1.xml', hl_protocol_dir / 'protocols/hyprland-global-shortcuts-v1.xml', + hl_protocol_dir / 'protocols/hyprland-input-capture-v1.xml', wl_protocol_dir / 'stable/linux-dmabuf/linux-dmabuf-v1.xml', ] diff --git a/src/core/PortalManager.cpp b/src/core/PortalManager.cpp index 9111870..a6d8589 100644 --- a/src/core/PortalManager.cpp +++ b/src/core/PortalManager.cpp @@ -1,7 +1,10 @@ #include "PortalManager.hpp" #include "../helpers/Log.hpp" #include "../helpers/MiscFunctions.hpp" +#include "wayland.hpp" +#include +#include #include #include #include @@ -9,6 +12,7 @@ #include #include +#include SOutput::SOutput(SP output_) : output(output_) { output->setName([this](CCWlOutput* o, const char* name_) { @@ -19,13 +23,19 @@ SOutput::SOutput(SP output_) : output(output_) { Debug::log(LOG, "Found output name {}", name); }); - output->setMode([this](CCWlOutput* r, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { // - refreshRate = refresh; - }); - output->setGeometry([this](CCWlOutput* r, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char* make, const char* model, - int32_t transform_) { // - transform = (wl_output_transform)transform_; + output->setMode([this](CCWlOutput* r, uint32_t flags, int32_t width, int32_t height, int32_t refresh) { + refreshRate = refresh; + this->width = width; + this->height = height; }); + output->setGeometry( + [this](CCWlOutput* r, int32_t x, int32_t y, int32_t physical_width, int32_t physical_height, int32_t subpixel, const char* make, const char* model, int32_t transform_) { + transform = (wl_output_transform)transform_; + this->x = x; + this->y = y; + }); + output->setScale([this](CCWlOutput* r, uint32_t factor) { this->scale = factor; }); + output->setDone([](CCWlOutput* r) { g_pPortalManager->m_sPortals.inputCapture->zonesChanged(); }); } CPortalManager::CPortalManager() { @@ -63,7 +73,9 @@ void CPortalManager::onGlobal(uint32_t name, const char* interface, uint32_t ver m_sPortals.globalShortcuts = std::make_unique(makeShared( (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_global_shortcuts_manager_v1_interface, version))); } - + if (INTERFACE == hyprland_input_capture_manager_v1_interface.name) + m_sPortals.inputCapture = std::make_unique(makeShared( + (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_input_capture_manager_v1_interface, version))); else if (INTERFACE == hyprland_toplevel_export_manager_v1_interface.name) { m_sWaylandConnection.hyprlandToplevelMgr = makeShared( (wl_proxy*)wl_registry_bind((wl_registry*)m_sWaylandConnection.registry->resource(), name, &hyprland_toplevel_export_manager_v1_interface, version)); @@ -417,6 +429,7 @@ void CPortalManager::startEventLoop() { m_sPortals.screencopy.reset(); m_sPortals.screenshot.reset(); m_sHelpers.toplevel.reset(); + m_sPortals.inputCapture.reset(); m_pConnection.reset(); pw_loop_destroy(m_sPipewire.loop); @@ -438,6 +451,10 @@ SOutput* CPortalManager::getOutputFromName(const std::string& name) { return nullptr; } +std::vector> const& CPortalManager::getAllOutputs() { + return m_vOutputs; +} + static char* gbm_find_render_node(drmDevice* device) { drmDevice* devices[64]; char* render_node = NULL; diff --git a/src/core/PortalManager.hpp b/src/core/PortalManager.hpp index e0f2f03..6ca241d 100644 --- a/src/core/PortalManager.hpp +++ b/src/core/PortalManager.hpp @@ -1,13 +1,16 @@ #pragma once +#include #include #include + #include #include "wayland.hpp" #include "../portals/Screencopy.hpp" #include "../portals/Screenshot.hpp" #include "../portals/GlobalShortcuts.hpp" +#include "../portals/InputCapture.hpp" #include "../helpers/Timer.hpp" #include "../shared/ToplevelManager.hpp" #include @@ -33,6 +36,9 @@ struct SOutput { uint32_t id = 0; float refreshRate = 60.0; wl_output_transform transform = WL_OUTPUT_TRANSFORM_NORMAL; + uint32_t width, height; + int32_t x, y; + int32_t scale; }; struct SDMABUFModifier { @@ -44,13 +50,14 @@ class CPortalManager { public: CPortalManager(); - void init(); + void init(); void onGlobal(uint32_t name, const char* interface, uint32_t version); void onGlobalRemoved(uint32_t name); - sdbus::IConnection* getConnection(); - SOutput* getOutputFromName(const std::string& name); + sdbus::IConnection* getConnection(); + SOutput* getOutputFromName(const std::string& name); + std::vector> const& getAllOutputs(); struct { pw_loop* loop = nullptr; @@ -60,6 +67,7 @@ class CPortalManager { std::unique_ptr screencopy; std::unique_ptr screenshot; std::unique_ptr globalShortcuts; + std::unique_ptr inputCapture; } m_sPortals; struct { diff --git a/src/main.cpp b/src/main.cpp index cdeb83d..e5c3ed3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,4 +42,4 @@ int main(int argc, char** argv, char** envp) { g_pPortalManager->init(); return 0; -} \ No newline at end of file +} diff --git a/src/meson.build b/src/meson.build index b76a85f..2f8d1e6 100644 --- a/src/meson.build +++ b/src/meson.build @@ -9,6 +9,7 @@ executable('xdg-desktop-portal-hyprland', dependency('hyprlang'), dependency('hyprutils'), dependency('libdrm'), + dependency('libeis-1.0'), dependency('libpipewire-0.3'), dependency('sdbus-c++'), dependency('threads'), diff --git a/src/portals/InputCapture.cpp b/src/portals/InputCapture.cpp new file mode 100644 index 0000000..8f9557f --- /dev/null +++ b/src/portals/InputCapture.cpp @@ -0,0 +1,596 @@ +#include "InputCapture.hpp" + +#include "../core/PortalManager.hpp" +#include "../helpers/Log.hpp" +#include "hyprland-input-capture-v1.hpp" +#include "shared/Session.hpp" +#include "src/shared/Eis.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +CInputCapturePortal::CInputCapturePortal(SP mgr) { + Debug::log(LOG, "[input-capture] initializing input capture portal"); + m_sState.manager = mgr; + sessionCounter = 0; + lastZoneSet = 0; + + mgr->setAbsoluteMotion([this](CCHyprlandInputCaptureManagerV1* r, wl_fixed_t x, wl_fixed_t y, wl_fixed_t dx, wl_fixed_t dy) { + onAbsoluteMotion(wl_fixed_to_double(x), wl_fixed_to_double(y), wl_fixed_to_double(dx), wl_fixed_to_double(dy)); + }); + + mgr->setKey([this](CCHyprlandInputCaptureManagerV1* r, uint32_t key, hyprlandInputCaptureManagerV1KeyState state) { onKey(key, state); }); + + mgr->setButton([this](CCHyprlandInputCaptureManagerV1* r, uint32_t button, hyprlandInputCaptureManagerV1ButtonState state) { onButton(button, state); }); + + mgr->setAxis([this](CCHyprlandInputCaptureManagerV1* r, hyprlandInputCaptureManagerV1Axis axis, double value) { onAxis(axis, value); }); + + mgr->setAxisValue120([this](CCHyprlandInputCaptureManagerV1* r, hyprlandInputCaptureManagerV1Axis axis, int32_t value120) { onAxis(axis, value120); }); + + mgr->setAxisStop([this](CCHyprlandInputCaptureManagerV1* r, hyprlandInputCaptureManagerV1Axis axis) { onAxisStop(axis); }); + + mgr->setFrame([this](CCHyprlandInputCaptureManagerV1* r) { onFrame(); }); + + m_pObject = sdbus::createObject(*g_pPortalManager->getConnection(), OBJECT_PATH); + + m_pObject->registerMethod(INTERFACE_NAME, "CreateSession", "oossa{sv}", "ua{sv}", [&](sdbus::MethodCall c) { onCreateSession(c); }); + m_pObject->registerMethod(INTERFACE_NAME, "GetZones", "oosa{sv}", "ua{sv}", [&](sdbus::MethodCall c) { onGetZones(c); }); + m_pObject->registerMethod(INTERFACE_NAME, "SetPointerBarriers", "oosa{sv}aa{sv}u", "ua{sv}", [&](sdbus::MethodCall c) { onSetPointerBarriers(c); }); + m_pObject->registerMethod(INTERFACE_NAME, "Enable", "osa{sv}", "ua{sv}", [&](sdbus::MethodCall c) { onEnable(c); }); + m_pObject->registerMethod(INTERFACE_NAME, "Disable", "osa{sv}", "ua{sv}", [&](sdbus::MethodCall c) { onDisable(c); }); + m_pObject->registerMethod(INTERFACE_NAME, "Release", "osa{sv}", "ua{sv}", [&](sdbus::MethodCall c) { onRelease(c); }); + m_pObject->registerMethod(INTERFACE_NAME, "ConnectToEIS", "osa{sv}", "h", [&](sdbus::MethodCall c) { onConnectToEIS(c); }); + + m_pObject->registerProperty(INTERFACE_NAME, "SupportedCapabilities", "u", [](sdbus::PropertyGetReply& reply) { reply << (uint)(1 | 2); }); + m_pObject->registerProperty(INTERFACE_NAME, "version", "u", [](sdbus::PropertyGetReply& reply) { reply << (uint)1; }); + + m_pObject->finishRegistration(); + + for (auto& o : g_pPortalManager->getAllOutputs()) { + Debug::log(LOG, "{} {}x{}", o->name, o->width, o->height); + } + + Debug::log(LOG, "[input-capture] init successful"); +} + +void complete(sdbus::MethodCall& call) { + auto reply = call.createReply(); + reply << (uint32_t)0; + reply << std::unordered_map{}; + reply.send(); +} + +void CInputCapturePortal::onCreateSession(sdbus::MethodCall& call) { + Debug::log(LOG, "[input-capture] New session:"); + + sdbus::ObjectPath requestHandle, sessionHandle; + + call >> requestHandle; + call >> sessionHandle; + + std::string appID; + call >> appID; + + std::string parentWindow; + call >> parentWindow; + + std::unordered_map options; + call >> options; + uint32_t capabilities = options["capabilities"]; + + Debug::log(LOG, "[input-capture] | {}", requestHandle.c_str()); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + Debug::log(LOG, "[input-capture] | parent_window: {}", parentWindow); + Debug::log(LOG, "[input-capture] | capabilities : {}", capabilities); + + std::string sessionId = "input-capture-" + std::to_string(sessionCounter++); + Debug::log(LOG, "[input-capture] | sessionId : {}", sessionId); + + const std::shared_ptr session = std::make_shared(); + + session->appid = appID; + session->requestHandle = requestHandle; + session->sessionHandle = sessionHandle; + session->sessionId = sessionId; + session->capabilities = capabilities; + session->activationId = 0; + session->status = CREATED; + + session->session = createDBusSession(sessionHandle); + session->session->onDestroy = [session, this]() { + if (session->status == ACTIVATED) { + disable(session->sessionHandle); + } + session->eis->stopServer(); + Debug::log(LOG, "[input-capture] Session {} destroyed", session->sessionHandle.c_str()); + + session->session.release(); + }; + + session->request = createDBusRequest(requestHandle); + session->request->onDestroy = [session]() { session->request.release(); }; + + session->eis = std::make_unique("eis-" + sessionId); + + sessions.emplace(sessionHandle, session); + + std::unordered_map results; + results["capabilities"] = (uint)3; + results["session_id"] = sessionId; + + auto reply = call.createReply(); + reply << (uint32_t)0; + reply << results; + reply.send(); +} + +void CInputCapturePortal::onGetZones(sdbus::MethodCall& call) { + Debug::log(LOG, "[input-capture] New GetZones request:"); + + sdbus::ObjectPath requestHandle, sessionHandle; + + call >> requestHandle; + call >> sessionHandle; + + std::string appID; + call >> appID; + + Debug::log(LOG, "[input-capture] | {}", requestHandle.c_str()); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return; + + std::vector> zones; + for (auto& o : g_pPortalManager->getAllOutputs()) { + zones.push_back(sdbus::Struct(o->width, o->height, o->x, o->y)); + } + + std::unordered_map results; + results["zones"] = zones; + results["zone_set"] = ++lastZoneSet; + + auto reply = call.createReply(); + reply << (uint32_t)0; + reply << results; + reply.send(); +} + +void CInputCapturePortal::onSetPointerBarriers(sdbus::MethodCall& call) { + Debug::log(LOG, "[input-capture] New SetPointerBarriers request:"); + + sdbus::ObjectPath requestHandle, sessionHandle; + + call >> requestHandle; + call >> sessionHandle; + + std::string appID; + call >> appID; + + Debug::log(LOG, "[input-capture] | {}", requestHandle.c_str()); + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return complete(call); + + std::unordered_map options; + call >> options; + + std::vector> barriers; + call >> barriers; + + uint32_t zoneSet; + call >> zoneSet; + Debug::log(LOG, "[input-capture] | zoneSet: {}", zoneSet); + + if (zoneSet != lastZoneSet) { + Debug::log(WARN, "[input-capture] Invalid zone set discarding barriers"); + complete(call); //TODO: We should return failed_barries + return; + } + + sessions[sessionHandle]->barriers.clear(); + for (const auto& b : barriers) { + uint id = b.at("barrier_id"); + int x1, y1, x2, y2; + + sdbus::Struct p = b.at("position"); + x1 = p.get<0>(); + y1 = p.get<1>(); + x2 = p.get<2>(); + y2 = p.get<3>(); + + Debug::log(LOG, "[input-capture] | barrier: {}, [{}, {}] [{}, {}]", id, x1, y1, x2, y2); + sessions[sessionHandle]->barriers[id] = {id, x1, y1, x2, y2}; + } + + std::vector failedBarriers; + + std::unordered_map results; + results["failed_barriers"] = failedBarriers; + + auto reply = call.createReply(); + reply << (uint32_t)0; + reply << results; + reply.send(); +} + +void CInputCapturePortal::onDisable(sdbus::MethodCall& call) { + Debug::log(LOG, "[input-capture] New Disable request:"); + + sdbus::ObjectPath sessionHandle; + call >> sessionHandle; + + std::string appID; + call >> appID; + + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return complete(call); + + disable(sessionHandle); + + complete(call); +} + +void CInputCapturePortal::onEnable(sdbus::MethodCall& call) { + Debug::log(LOG, "[input-capture] New Enable request:"); + + sdbus::ObjectPath sessionHandle; + call >> sessionHandle; + + std::string appID; + call >> appID; + + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return complete(call); + + sessions[sessionHandle]->status = ENABLED; + + complete(call); +} + +void CInputCapturePortal::onRelease(sdbus::MethodCall& call) { + Debug::log(LOG, "[input-capture] New Release request:"); + + sdbus::ObjectPath sessionHandle; + call >> sessionHandle; + + std::string appID; + call >> appID; + + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return complete(call); + + std::unordered_map options; + call >> options; + uint32_t activationId = options["activation_id"]; + if (activationId != sessions[sessionHandle]->activationId) { + Debug::log(WARN, "[input-capture] Invalid activation id {} expected {}", activationId, sessions[sessionHandle]->activationId); + complete(call); + return; + } + + deactivate(sessionHandle); + + //TODO: maybe warp pointer + + complete(call); +} + +void CInputCapturePortal::onConnectToEIS(sdbus::MethodCall& call) { + Debug::log(LOG, "[input-capture] New ConnectToEIS request:"); + + sdbus::ObjectPath sessionHandle; + call >> sessionHandle; + + std::string appID; + call >> appID; + + std::unordered_map options; + call >> options; + + Debug::log(LOG, "[input-capture] | {}", sessionHandle.c_str()); + Debug::log(LOG, "[input-capture] | appid: {}", appID); + + if (!sessionValid(sessionHandle)) + return complete(call); + + int sockfd = sessions[sessionHandle]->eis->getFileDescriptor(); + + Debug::log(LOG, "[input-capture] Connected to the socket. File descriptor: {}", sockfd); + auto reply = call.createReply(); + reply << (sdbus::UnixFd)sockfd; + reply.send(); +} + +bool CInputCapturePortal::sessionValid(sdbus::ObjectPath sessionHandle) { + if (!sessions.contains(sessionHandle)) { + Debug::log(WARN, "[input-capture] Unknown session handle: {}", sessionHandle.c_str()); + return false; + } + + return sessions[sessionHandle]->status != STOPPED; +} + +bool get_line_intersection(double p0_x, double p0_y, double p1_x, double p1_y, double p2_x, double p2_y, double p3_x, double p3_y, double* i_x, double* i_y) { + float s1_x, s1_y, s2_x, s2_y; + s1_x = p1_x - p0_x; + s1_y = p1_y - p0_y; + s2_x = p3_x - p2_x; + s2_y = p3_y - p2_y; + + float s, t; + s = (-s1_y * (p0_x - p2_x) + s1_x * (p0_y - p2_y)) / (-s2_x * s1_y + s1_x * s2_y); + t = (s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / (-s2_x * s1_y + s1_x * s2_y); + + if (s >= 0 && s <= 1 && t >= 0 && t <= 1) { + // Collision detected + if (i_x != NULL) + *i_x = p0_x + (t * s1_x); + if (i_y != NULL) + *i_y = p0_y + (t * s1_y); + return 1; + } + + return 0; // No collision +} + +bool testCollision(Barrier barrier, double px, double py, double nx, double ny) { + return get_line_intersection(barrier.x1, barrier.y1, barrier.x2, barrier.y2, px, py, nx, ny, nullptr, nullptr); +} + +uint32_t CInputCapturePortal::Session::isColliding(double px, double py, double nx, double ny) { + for (const auto& [key, value] : barriers) { + if (testCollision(value, px, py, nx, ny)) { + return key; + } + } + + return 0; +} + +void CInputCapturePortal::onAbsoluteMotion(double x, double y, double dx, double dy) { + for (const auto& [key, session] : sessions) { + int matched = session->isColliding(x, y, x - dx, y - dy); + if (matched != 0) { + activate(key, x, y, matched); + } + session->motion(dx, dy); + } +} + +void CInputCapturePortal::onKey(uint32_t key, bool pressed) { + for (const auto& [_, value] : sessions) { + value->key(key, pressed); + } +} + +void CInputCapturePortal::onButton(uint32_t button, bool pressed) { + for (const auto& [_, session] : sessions) { + session->button(button, pressed); + } +} + +void CInputCapturePortal::onAxis(bool axis, double value) { + for (const auto& [_, session] : sessions) { + session->axis(axis, value); + } +} + +void CInputCapturePortal::onAxisValue120(bool axis, int32_t value120) { + for (const auto& [_, session] : sessions) { + session->axisValue120(axis, value120); + } +} + +void CInputCapturePortal::onAxisStop(bool axis) { + for (const auto& [_, session] : sessions) { + session->axisStop(axis); + } +} + +void CInputCapturePortal::onFrame() { + for (const auto& [_, session] : sessions) { + session->frame(); + } +} + +void CInputCapturePortal::activate(sdbus::ObjectPath sessionHandle, double x, double y, uint32_t borderId) { + if (!sessionValid(sessionHandle)) + return; + + auto session = sessions[sessionHandle]; + if (!session->activate(x, y, borderId)) + return; + + auto signal = m_pObject->createSignal(INTERFACE_NAME, "Activated"); + signal << sessionHandle; + + g_pPortalManager->m_sPortals.inputCapture->INTERFACE_NAME; + + std::unordered_map results; + results["activation_id"] = session->activationId; + results["cursor_position"] = sdbus::Struct(x, y); + results["barrier_id"] = borderId; + signal << results; + + m_pObject->emitSignal(signal); +} + +bool CInputCapturePortal::Session::activate(double x, double y, uint32_t borderId) { + if (status != ENABLED) { + return false; + } + + activationId += 5; + status = ACTIVATED; + Debug::log(LOG, "[input-capture] Input captured for {} activationId: {}", sessionHandle.c_str(), activationId); + eis->startEmulating(activationId); + //TODO: capture the pointer + + return true; +} + +void CInputCapturePortal::deactivate(sdbus::ObjectPath sessionHandle) { + if (!sessionValid(sessionHandle)) + return; + + auto session = sessions[sessionHandle]; + if (!session->deactivate()) + return; + + auto signal = m_pObject->createSignal(INTERFACE_NAME, "Deactivated"); + signal << sessionHandle; + std::unordered_map options; + options["activation_id"] = session->activationId; + signal << options; + + m_pObject->emitSignal(signal); + + //TODO: release the pointer +} + +bool CInputCapturePortal::Session::deactivate() { + if (status != ACTIVATED) { + return false; + } + + Debug::log(LOG, "[input-capture] Input released for {}", sessionHandle.c_str()); + eis->stopEmulating(); + + status = ENABLED; + + return true; +} + +void CInputCapturePortal::zonesChanged() { + if (sessions.empty()) + return; + + Debug::log(LOG, "[input-capture] Monitor layout has changed, notifing clients"); + + for (auto& [key, value] : sessions) { + if (!value->zoneChanged()) + continue; + + auto signal = m_pObject->createSignal(INTERFACE_NAME, "Deactivated"); + signal << key; + + std::unordered_map options; + signal << options; + + m_pObject->emitSignal(signal); + } +} + +bool CInputCapturePortal::Session::zoneChanged() { + //TODO: notify EIS + return true; +} + +void CInputCapturePortal::disable(sdbus::ObjectPath sessionHandle) { + if (!sessionValid(sessionHandle)) + return; + + auto session = sessions[sessionHandle]; + if (!session->disable()) + return; + + if (session->status == ACTIVATED) + deactivate(sessionHandle); + + auto signal = m_pObject->createSignal(INTERFACE_NAME, "Disable"); + signal << sessionHandle; + + std::unordered_map options; + signal << options; + + m_pObject->emitSignal(signal); +} + +bool CInputCapturePortal::Session::disable() { + status = STOPPED; + + Debug::log(LOG, "[input-capture] Session {} disabled", sessionHandle.c_str()); + return true; +} + +void CInputCapturePortal::Session::motion(double dx, double dy) { + if (status != ACTIVATED) + return; + + eis->sendMotion(dx, dy); +} + +void CInputCapturePortal::Session::key(uint32_t key, bool pressed) { + if (status != ACTIVATED) + return; + + eis->sendKey(key, pressed); +} + +void CInputCapturePortal::Session::button(uint32_t button, bool pressed) { + if (status != ACTIVATED) + return; + + eis->sendButton(button, pressed); +} + +void CInputCapturePortal::Session::axis(bool axis, double value) { + if (status != ACTIVATED) + return; + + double x = 0; + double y = 0; + + if (axis) { + x = value; + } else { + y = value; + } + + eis->sendScrollDelta(x, y); +} + +void CInputCapturePortal::Session::axisValue120(bool axis, int32_t value) { + if (status != ACTIVATED) + return; + + int32_t x = 0; + int32_t y = 0; + + if (axis) { + x = value; + } else { + y = value; + } + + eis->sendScrollDiscrete(x, y); +} + +void CInputCapturePortal::Session::axisStop(bool axis) { + eis->sendScrollStop(axis, !axis); +} + +void CInputCapturePortal::Session::frame() { + eis->sendPointerFrame(); +} diff --git a/src/portals/InputCapture.hpp b/src/portals/InputCapture.hpp new file mode 100644 index 0000000..0ab78d1 --- /dev/null +++ b/src/portals/InputCapture.hpp @@ -0,0 +1,96 @@ +#pragma once +#include "hyprland-input-capture-v1.hpp" +#include "shared/Eis.hpp" +#include +#include +#include +#include +#include +#include +#include "../includes.hpp" +#include "../shared/Session.hpp" + +typedef int ClientStatus; +const ClientStatus CREATED = 0; //Is ready to be activated +const ClientStatus ENABLED = 1; //Is ready for receiving inputs +const ClientStatus ACTIVATED = 2; //Currently receiving inputs +const ClientStatus STOPPED = 3; //Can no longer be activated + +struct Barrier { + uint id; + int x1, y1, x2, y2; +}; + +class CInputCapturePortal { + public: + CInputCapturePortal(SP mgr); + + void onCreateSession(sdbus::MethodCall& methodCall); + void onGetZones(sdbus::MethodCall& methodCall); + void onSetPointerBarriers(sdbus::MethodCall& methodCall); + void onEnable(sdbus::MethodCall& methodCall); + void onDisable(sdbus::MethodCall& methodCall); + void onRelease(sdbus::MethodCall& methodCall); + void onConnectToEIS(sdbus::MethodCall& methodCall); + + void onAbsoluteMotion(double x, double y, double dx, double dy); + void onKey(uint32_t key, bool pressed); + void onButton(uint32_t button, bool pressed); + void onAxis(bool axis, double value); + void onAxisValue120(bool axis, int32_t value120); + void onAxisStop(bool axis); + void onFrame(); + + void zonesChanged(); + + struct Session { + std::string appid; + sdbus::ObjectPath requestHandle, sessionHandle; + std::string sessionId; + uint32_t capabilities; + + std::unique_ptr request; + std::unique_ptr session; + std::unique_ptr eis; + + std::unordered_map barriers; + uint32_t activationId; + ClientStatus status; + + // + bool activate(double x, double y, uint32_t borderId); + bool deactivate(); + bool disable(); + bool zoneChanged(); + + void motion(double dx, double dy); + void key(uint32_t key, bool pressed); + void button(uint32_t button, bool pressed); + void axis(bool axis, double value); + void axisValue120(bool axis, int32_t value120); + void axisStop(bool axis); + void frame(); + + uint32_t isColliding(double px, double py, double nx, double ny); + }; + + private: + struct { + SP manager; + } m_sState; + + std::unordered_map> sessions; + // + std::unique_ptr m_pObject; + uint sessionCounter; + uint lastZoneSet; + + const std::string INTERFACE_NAME = "org.freedesktop.impl.portal.InputCapture"; + const std::string OBJECT_PATH = "/org/freedesktop/portal/desktop"; + + bool sessionValid(sdbus::ObjectPath sessionHandle); + + void activate(sdbus::ObjectPath sessionHandle, double x, double y, uint32_t borderId); + void deactivate(sdbus::ObjectPath sessionHandle); + void disable(sdbus::ObjectPath sessionHandle); +}; diff --git a/src/shared/Eis.cpp b/src/shared/Eis.cpp new file mode 100644 index 0000000..2bab58c --- /dev/null +++ b/src/shared/Eis.cpp @@ -0,0 +1,287 @@ +#include "Eis.hpp" +#include "core/PortalManager.hpp" +#include "src/helpers/Log.hpp" +#include +#include +#include +#include + +EmulatedInputServer::EmulatedInputServer(std::string socketName) { + Debug::log(LOG, "[EIS] init socket: {}", socketName); + + const char* xdg = getenv("XDG_RUNTIME_DIR"); + if (xdg) + socketPath = std::string(xdg) + "/" + socketName; + + if (socketPath.empty()) { + Debug::log(ERR, "[EIS] Socket path is empty"); + return; + } + + client.handle = NULL; + client.seat = NULL; + client.pointer = NULL; + client.keyboard = NULL; + eis = eis_new(NULL); + + if (eis_setup_backend_socket(eis, socketPath.c_str())) { + Debug::log(ERR, "[EIS] Cannot init eis socket on {}", socketPath); + return; + } + Debug::log(LOG, "[EIS] Listening on {}", socketPath); + + stop = false; + std::thread thread(&EmulatedInputServer::listen, this); + thread.detach(); +} + +void EmulatedInputServer::listen() { + struct pollfd fds = { + .fd = eis_get_fd(eis), + .events = POLLIN, + .revents = 0, + }; + int nevents; + //Pull foverer events + while (!stop && (nevents = poll(&fds, 1, 1000)) > -1) { + eis_dispatch(eis); + + //Pull every availaible events + while (true) { + eis_event* e = eis_get_event(eis); + + if (!e) { + eis_event_unref(e); + break; + } + + int rc = onEvent(e); + eis_event_unref(e); + if (rc != 0) + break; + } + } +} + +int EmulatedInputServer::onEvent(eis_event* e) { + eis_client* client; + eis_seat* seat; + eis_device* device; + + switch (eis_event_get_type(e)) { + case EIS_EVENT_CLIENT_CONNECT: + client = eis_event_get_client(e); + Debug::log(LOG, "[EIS] {} client connected: {}", eis_client_is_sender(client) ? "sender" : "receiver", eis_client_get_name(client)); + + if (eis_client_is_sender(client)) { + Debug::log(WARN, "[EIS] Unexpected sender client {} connected to input capture session", eis_client_get_name(client)); + eis_client_disconnect(client); + return 0; + } + + if (this->client.handle != nullptr) { + Debug::log(WARN, "[EIS] Unexpected additional client {} connected to input capture session", eis_client_get_name(client)); + eis_client_disconnect(client); + return 0; + } + + this->client.handle = client; + + eis_client_connect(client); + Debug::log(LOG, "[EIS] creating new default seat"); + seat = eis_client_new_seat(client, "default"); + + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_POINTER); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_BUTTON); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_SCROLL); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_KEYBOARD); + eis_seat_add(seat); + this->client.seat = seat; + break; + case EIS_EVENT_CLIENT_DISCONNECT: + client = eis_event_get_client(e); + Debug::log(LOG, "[EIS] {} disconnected", eis_client_get_name(client)); + eis_client_disconnect(client); + + eis_seat_unref(this->client.seat); + clearPointer(); + clearKeyboard(); + this->client.handle = NULL; + break; + case EIS_EVENT_SEAT_BIND: + Debug::log(LOG, "[EIS] Binding seats..."); + + if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_POINTER) && eis_event_seat_has_capability(e, EIS_DEVICE_CAP_BUTTON) && + eis_event_seat_has_capability(e, EIS_DEVICE_CAP_SCROLL)) + ensurePointer(e); + else + clearPointer(); + + if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_KEYBOARD)) + ensureKeyboard(e); + else + clearKeyboard(); + break; + case EIS_EVENT_DEVICE_CLOSED: + device = eis_event_get_device(e); + if (device == this->client.pointer) { + clearPointer(); + } else if (device == this->client.keyboard) { + Debug::log(LOG, "[EIS] Clearing keyboard"); + clearKeyboard(); + } else { + Debug::log(WARN, "[EIS] Unknown device to close"); + } + break; + case EIS_EVENT_FRAME: Debug::log(LOG, "[EIS] Got event EIS_EVENT_FRAME"); break; + case EIS_EVENT_DEVICE_START_EMULATING: Debug::log(LOG, "[EIS] Got event EIS_EVENT_DEVICE_START_EMULATING"); break; + case EIS_EVENT_DEVICE_STOP_EMULATING: Debug::log(LOG, "[EIS] Got event EIS_EVENT_DEVICE_STOP_EMULATING"); break; + case EIS_EVENT_POINTER_MOTION: Debug::log(LOG, "[EIS] Got event EIS_EVENT_POINTER_MOTION"); break; + case EIS_EVENT_POINTER_MOTION_ABSOLUTE: Debug::log(LOG, "[EIS] Got event EIS_EVENT_POINTER_MOTION_ABSOLUTE"); break; + case EIS_EVENT_BUTTON_BUTTON: Debug::log(LOG, "[EIS] Got event EIS_EVENT_BUTTON_BUTTON"); break; + case EIS_EVENT_SCROLL_DELTA: Debug::log(LOG, "[EIS] Got event EIS_EVENT_SCROLL_DELTA"); break; + case EIS_EVENT_SCROLL_STOP: Debug::log(LOG, "[EIS] Got event EIS_EVENT_SCROLL_STOP"); break; + case EIS_EVENT_SCROLL_CANCEL: Debug::log(LOG, "[EIS] Got event EIS_EVENT_SCROLL_CANCEL"); break; + case EIS_EVENT_SCROLL_DISCRETE: Debug::log(LOG, "[EIS] Got event EIS_EVENT_SCROLL_DISCRETE"); break; + case EIS_EVENT_KEYBOARD_KEY: Debug::log(LOG, "[EIS] Got event EIS_EVENT_KEYBOARD_KEY"); break; + case EIS_EVENT_TOUCH_DOWN: Debug::log(LOG, "[EIS] Got event EIS_EVENT_TOUCH_DOWN"); break; + case EIS_EVENT_TOUCH_UP: Debug::log(LOG, "[EIS] Got event EIS_EVENT_TOUCH_UP"); break; + case EIS_EVENT_TOUCH_MOTION: Debug::log(LOG, "[EIS] Got event EIS_EVENT_TOUCH_MOTION"); break; + } + return 0; +} + +void EmulatedInputServer::ensurePointer(eis_event* event) { + if (client.pointer != nullptr) + return; + + struct eis_device* pointer = eis_seat_new_device(client.seat); + eis_device_configure_name(pointer, "captured relative pointer"); + eis_device_configure_capability(pointer, EIS_DEVICE_CAP_POINTER); + eis_device_configure_capability(pointer, EIS_DEVICE_CAP_BUTTON); + eis_device_configure_capability(pointer, EIS_DEVICE_CAP_SCROLL); + + for (auto& o : g_pPortalManager->getAllOutputs()) { + struct eis_region* r = eis_device_new_region(pointer); + + eis_region_set_offset(r, o->x, o->y); + eis_region_set_size(r, o->width, o->height); + eis_region_set_physical_scale(r, o->scale); + eis_region_add(r); + eis_region_unref(r); + } + + eis_device_add(pointer); + eis_device_resume(pointer); + + client.pointer = pointer; +} + +void EmulatedInputServer::ensureKeyboard(eis_event* event) { + if (client.keyboard != nullptr) + return; + + struct eis_device* keyboard = eis_seat_new_device(client.seat); + eis_device_configure_name(keyboard, "captured keyboard"); + eis_device_configure_capability(keyboard, EIS_DEVICE_CAP_KEYBOARD); + // TODO: layout + eis_device_add(keyboard); + eis_device_resume(keyboard); + + client.keyboard = keyboard; +} + +//TODO: remove and re-add devices when monitors change (see: mutter/meta-input-capture-session.c:1107) + +void EmulatedInputServer::clearPointer() { + if (client.pointer == nullptr) + return; + Debug::log(LOG, "[EIS] Clearing pointer"); + + eis_device_remove(client.pointer); + eis_device_unref(client.pointer); + client.pointer = nullptr; +} + +void EmulatedInputServer::clearKeyboard() { + if (client.keyboard == nullptr) + return; + Debug::log(LOG, "[EIS] Clearing keyboard"); + + eis_device_remove(client.keyboard); + eis_device_unref(client.keyboard); + client.keyboard = nullptr; +} + +int EmulatedInputServer::getFileDescriptor() { + return eis_backend_fd_add_client(eis); +} + +void EmulatedInputServer::startEmulating(int sequence) { + Debug::log(LOG, "[EIS] Start Emulating"); + + if (client.pointer != nullptr) + eis_device_start_emulating(client.pointer, sequence); + + if (client.keyboard != nullptr) + eis_device_start_emulating(client.keyboard, sequence); +} + +void EmulatedInputServer::stopEmulating() { + Debug::log(LOG, "[EIS] Stop Emulating"); + + if (client.pointer != nullptr) + eis_device_stop_emulating(client.pointer); + + if (client.keyboard != nullptr) + eis_device_stop_emulating(client.keyboard); +} + +void EmulatedInputServer::sendMotion(double x, double y) { + if (client.pointer == nullptr) + return; + eis_device_pointer_motion(client.pointer, x, y); +} + +void EmulatedInputServer::sendKey(uint32_t key, bool pressed) { + if (client.keyboard == nullptr) + return; + uint64_t now = eis_now(eis); + eis_device_keyboard_key(client.keyboard, key, pressed); + eis_device_frame(client.keyboard, now); +} + +void EmulatedInputServer::sendButton(uint32_t button, bool pressed) { + if (client.pointer == nullptr) + return; + eis_device_button_button(client.pointer, button, pressed); +} + +void EmulatedInputServer::sendScrollDiscrete(int32_t x, int32_t y) { + if (client.pointer == nullptr) + return; + eis_device_scroll_discrete(client.pointer, x, y); +} + +void EmulatedInputServer::sendScrollDelta(double x, double y) { + if (client.pointer == nullptr) + return; + eis_device_scroll_delta(client.pointer, x, y); +} + +void EmulatedInputServer::sendScrollStop(bool x, bool y) { + if (client.pointer == nullptr) + return; + eis_device_scroll_stop(client.pointer, x, y); +} + +void EmulatedInputServer::sendPointerFrame() { + if (client.pointer == nullptr) + return; + uint64_t now = eis_now(eis); + eis_device_frame(client.pointer, now); +} + +void EmulatedInputServer::stopServer() { + stop = true; +} diff --git a/src/shared/Eis.hpp b/src/shared/Eis.hpp new file mode 100644 index 0000000..63b4e78 --- /dev/null +++ b/src/shared/Eis.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include + +struct EisClient { + struct eis_client* handle; + struct eis_seat* seat; + + struct eis_device* pointer; + struct eis_device* keyboard; +}; + +/* + * Responsible to creating a socket for input communication + */ +class EmulatedInputServer { + public: + EmulatedInputServer(std::string socketPath); + std::string socketPath; + + void startEmulating(int activationId); + void stopEmulating(); + + void sendMotion(double x, double y); + void sendKey(uint32_t key, bool pressed); + void sendButton(uint32_t button, bool pressed); + void sendScrollDelta(double x, double y); + void sendScrollDiscrete(int32_t x, int32_t y); + void sendScrollStop(bool stopX, bool stopY); + void sendPointerFrame(); + + int getFileDescriptor(); + + void stopServer(); + + private: + bool stop; + struct eis* eis; + EisClient client; + + int onEvent(eis_event* e); + void listen(); + void ensurePointer(eis_event* event); + void ensureKeyboard(eis_event* event); + void clearPointer(); + void clearKeyboard(); +}; diff --git a/subprojects/hyprland-protocols b/subprojects/hyprland-protocols index 4d29e48..53a994b 160000 --- a/subprojects/hyprland-protocols +++ b/subprojects/hyprland-protocols @@ -1 +1 @@ -Subproject commit 4d29e48433270a2af06b8bc711ca1fe5109746cd +Subproject commit 53a994b2efbcc19862125fc9a8d5a752a24a0f20