Skip to content

Commit

Permalink
feat: long press gesture (#70)
Browse files Browse the repository at this point in the history
* feat: configurable timeout for gestures

* tests: add timeout duration

* fix: bad math

* feat: add hold gestures action

* tests: add tests for hold gestures

* internal: rework CallbackAction to trigger immediately

* fixup! internal: rework CallbackAction to trigger immediately

* feat: hold gesture emits drag and completed events

* refactor: rename

* refactor: separate drag gestures from completed ones

* tests: separate drag gestures from completed one

* feat: add explicit drag event handlers

* refactor: rename

* feat: handle drag gestures

* fix: use exisiting methods for updating workspace swipe

* fix: add long press gesture

* fix: handle hold gestures

* feat: expose function to handle end of drag separately

* tests: use updated end of drag function

* tests: only handle drag gestures if needed

* cleanup

* fixup! feat: expose function to handle end of drag separately

* fix: handle drag gesture end

* dev: add hotreload script

* fix: cascade reset calls to child of OnCompleteAction

* internal: add onLongPressTimeout()

* internal: expose new API for long press timers

* feat: impl hold gesture timer

* fix: cleanup timer source in dtor

* refactor: rename

* fix: operator precedence bug

* fix: move cursor during dragGestureUpdate

* fixup! fix: move cursor during dragGestureUpdate

* refactor: rename "hold" to "long press"

* chore: update README
  • Loading branch information
horriblename committed Dec 26, 2023
1 parent ebc604b commit 7ccb8d3
Show file tree
Hide file tree
Showing 10 changed files with 579 additions and 152 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ plugin {
# must be >= 3
workspace_swipe_fingers = 3
# in milliseconds
long_press_delay = 400
experimental {
# send proper cancel events to windows instead of hacky touch_up events,
# NOT recommended as it crashed a few times, once it's stabilized I'll make it the default
Expand Down Expand Up @@ -152,8 +155,7 @@ where (skip to [examples](#examples) if this is confusing):
3. `edge:<from_edge>:<direction>`
- `<from_edge>` is from which edge to start from (l/r/u/d)
- `<direction>` is in which direction to swipe (l/r/u/d/lu/ld/ru/rd)
> :warning: `<gesture_name>` with misspellings will be silently ignored.
4. `longpress:<finger_count>`
#### Examples
Expand All @@ -178,6 +180,10 @@ bind = , swipe:3:ld, exec, foot
# tap with 3 fingers
# NOTE: tap events only trigger for finger count of >= 3
bind = , tap:3, exec, foot
# longpress can trigger mouse binds:
bindm = , longpress:2, movewindow
bindm = , longpress:3, resizewindow
```
# Acknowledgements
Expand Down
50 changes: 50 additions & 0 deletions scripts/hotreload
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/sh

set -e

TMP=/tmp/hyprgrass-testing
helpText="
usage: $0 FILE
Hot reload a hyprland plugin
Plugins loaded by this script will not be 'cached' (not really but don't worry about it :D).
If a new plugin with the same file name is loaded, the old one is first unloaded.
example: $(basename $0) ./build/libhyprgrass.so
Subsequent calls will automatically unload the plugin as needed"

error () {
echo "$helpText"
exit 1
}

if [ -z "$1" ]; then
error
fi

if ! [ -f "$1" ]; then
echo "'$1' is not a valid file"
echo ''
error
fi

baseFileName="$(basename "$1")"
fileHash="$(sha256sum "$1" | cut -d ' ' -f 1)"
pluginTempDir="$TMP/$fileHash"
pluginTempFile="$pluginTempDir/$baseFileName"

for soPath in $TMP/*/$baseFileName; do
if [ -f "$soPath" ]; then
hyprctl plugin unload "$soPath"
rm -r $(dirname "$soPath")
fi
done

mkdir -p "$pluginTempDir"

cp "$1" "$pluginTempDir"

hyprctl plugin load "$pluginTempFile"

149 changes: 124 additions & 25 deletions src/GestureManager.cpp
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
#include "GestureManager.hpp"
#include "hyprland/src/managers/LayoutManager.hpp"
#include "wayfire/touch/touch.hpp"
#include <algorithm>
#include <cstdint>
#include <hyprland/src/Compositor.hpp>
#include <hyprland/src/debug/Log.hpp>
#include <hyprland/src/managers/KeybindManager.hpp>
#include <hyprland/src/managers/input/InputManager.hpp>
#include <memory>
#include <optional>

// constexpr double SWIPE_THRESHOLD = 30.;

bool handleGestureBind(std::string bind, bool pressed);

int handleLongPressTimer(void* data) {
const auto gesture_manager = (GestureManager*)data;
gesture_manager->onLongPressTimeout(gesture_manager->long_press_next_trigger_time);

return 0;
}

GestureManager::GestureManager() {
static auto* const PSENSITIVITY =
&HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:sensitivity")->floatValue;
static auto* const LONG_PRESS_DELAY =
&HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:long_press_delay")->intValue;

this->addMultiFingerGesture(PSENSITIVITY, LONG_PRESS_DELAY);
this->addMultiFingerTap(PSENSITIVITY, LONG_PRESS_DELAY);
this->addLongPress(PSENSITIVITY, LONG_PRESS_DELAY);
this->addEdgeSwipeGesture(PSENSITIVITY, LONG_PRESS_DELAY);

this->long_press_timer = wl_event_loop_add_timer(g_pCompositor->m_sWLEventLoop, handleLongPressTimer, this);
}

this->addMultiFingerGesture(PSENSITIVITY);
this->addMultiFingerTap(PSENSITIVITY);
this->addEdgeSwipeGesture(PSENSITIVITY);
GestureManager::~GestureManager() {
wl_event_source_remove(this->long_press_timer);
}

void GestureManager::emulateSwipeBegin(uint32_t time) {
Expand Down Expand Up @@ -60,24 +81,32 @@ void GestureManager::emulateSwipeUpdate(uint32_t time) {
m_vGestureLastCenter = currentCenter;
}

bool GestureManager::handleGesture(const CompletedGesture& gev) {
if (gev.type == TouchGestureType::SWIPE_HOLD) {
return this->handleWorkspaceSwipe(gev);
}
if (gev.type == TouchGestureType::SWIPE && this->dragGestureIsActive()) {
this->emulateSwipeEnd(0, false);
return true;
bool GestureManager::handleCompletedGesture(const CompletedGesture& gev) {
return handleGestureBind(gev.to_string(), false);
}

bool GestureManager::handleDragGesture(const DragGesture& gev) {
switch (gev.type) {
case DragGestureType::SWIPE:
return this->handleWorkspaceSwipe(gev);
default:
break;
}

const auto bind = gev.to_string();
bool found = false;
return handleGestureBind(gev.to_string(), true);
}

// bind is the name of the gesture event.
// pressed only matters for mouse binds: only start of drag gestures should set it to true
bool handleGestureBind(std::string bind, bool pressed) {
bool found = false;
Debug::log(LOG, "[hyprgrass] Gesture Triggered: {}", bind);

for (const auto& k : g_pKeybindManager->m_lKeybinds) {
if (k.key != bind)
continue;

const auto DISPATCHER = g_pKeybindManager->m_mDispatchers.find(k.handler);
const auto DISPATCHER = g_pKeybindManager->m_mDispatchers.find(k.mouse ? "mouse" : k.handler);

// Should never happen, as we check in the ConfigManager, but oh well
if (DISPATCHER == g_pKeybindManager->m_mDispatchers.end()) {
Expand All @@ -91,28 +120,78 @@ bool GestureManager::handleGesture(const CompletedGesture& gev) {
if (k.handler == "pass")
continue;

DISPATCHER->second(k.arg);
found = true;
if (k.handler == "mouse") {
DISPATCHER->second((pressed ? "1" : "0") + k.arg);
} else {
DISPATCHER->second(k.arg);
}

if (!k.nonConsuming) {
found = true;
}
}

return found;
}

void GestureManager::handleCancelledGesture() {
if (!this->dragGestureIsActive()) {
if (!this->getActiveDragGesture().has_value()) {
return;
}

// FIXME: make it so handleDragGestureEnd is called instead of handling this here
switch (this->getActiveDragGesture()->type) {
case DragGestureType::SWIPE:
this->emulateSwipeEnd(0, false);
return;
case DragGestureType::LONG_PRESS:
break;
}
}

void GestureManager::dragGestureUpdate(const wf::touch::gesture_event_t& ev) {
if (!this->getActiveDragGesture().has_value()) {
return;
}

switch (this->getActiveDragGesture()->type) {
case DragGestureType::SWIPE:
emulateSwipeUpdate(ev.time);
return;
case DragGestureType::LONG_PRESS: {
const auto pos = this->m_sGestureState.get_center().current;
wlr_cursor_warp(g_pCompositor->m_sWLRCursor, nullptr, pos.x, pos.y);
// HACK: g_pInputManager->onMouseMoveUnified is private so I'm using just this
g_pLayoutManager->getCurrentLayout()->onMouseMove(Vector2D(pos.x, pos.y));
return;
}
}
}

void GestureManager::handleDragGestureEnd(const DragGesture& gev) {
if (!this->getActiveDragGesture().has_value()) {
return;
}

this->emulateSwipeEnd(0, false);
switch (this->getActiveDragGesture()->type) {
case DragGestureType::SWIPE:
emulateSwipeEnd(0, false);
return;
case DragGestureType::LONG_PRESS:
break;
}

handleGestureBind(gev.to_string(), false);
}

bool GestureManager::handleWorkspaceSwipe(const CompletedGesture& gev) {
bool GestureManager::handleWorkspaceSwipe(const DragGesture& gev) {
static auto* const PWORKSPACEFINGERS =
&HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:workspace_swipe_fingers")->intValue;
const auto VERTANIMS = g_pCompositor->getWorkspaceByID(g_pCompositor->m_pLastMonitor->activeWorkspace)
->m_vRenderOffset.getConfig()
->pValues->internalStyle == "slidevert";

if (gev.type == TouchGestureType::SWIPE_HOLD && gev.finger_count == *PWORKSPACEFINGERS) {
if (gev.type == DragGestureType::SWIPE && gev.finger_count == *PWORKSPACEFINGERS) {
const auto horizontal = GESTURE_DIRECTION_LEFT | GESTURE_DIRECTION_RIGHT;
const auto vertical = GESTURE_DIRECTION_UP | GESTURE_DIRECTION_DOWN;
const auto workspace_directions = VERTANIMS ? vertical : horizontal;
Expand All @@ -129,6 +208,15 @@ bool GestureManager::handleWorkspaceSwipe(const CompletedGesture& gev) {
return false;
}

void GestureManager::updateLongPressTimer(uint32_t current_time, uint32_t delay) {
this->long_press_next_trigger_time = current_time + delay + 1;
wl_event_source_timer_update(this->long_press_timer, delay);
}

void GestureManager::stopLongPressTimer() {
wl_event_source_timer_update(this->long_press_timer, 0);
}

void GestureManager::sendCancelEventsToWindows() {
static auto* const SEND_CANCEL =
&HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:experimental:send_cancel")->intValue;
Expand Down Expand Up @@ -198,8 +286,6 @@ bool GestureManager::onTouchUp(wlr_touch_up_event* ev) {
if (g_pCompositor->m_sSeat.exclusiveClient) // lock screen, I think
return false;

// NOTE this is neccessary because onTouchDown might fail and exit without
// updating gestures
wf::touch::point_t lift_off_pos;
try {
lift_off_pos = this->m_sGestureState.fingers.at(ev->touch_id).current;
Expand Down Expand Up @@ -227,10 +313,6 @@ bool GestureManager::onTouchMove(wlr_touch_motion_event* ev) {
if (g_pCompositor->m_sSeat.exclusiveClient) // lock screen, I think
return false;

if (this->dragGestureIsActive()) {
this->emulateSwipeUpdate(0);
}

auto pos = wlrTouchEventPositionAsPixels(ev->x, ev->y);

const wf::touch::gesture_event_t gesture_event = {
Expand All @@ -247,6 +329,23 @@ SMonitorArea GestureManager::getMonitorArea() const {
return this->m_sMonitorArea;
}

void GestureManager::onLongPressTimeout(uint32_t time_msec) {
if (this->m_sGestureState.fingers.empty()) {
return;
}

const auto finger = this->m_sGestureState.fingers.begin();

const wf::touch::gesture_event_t touch_event = {
.type = wf::touch::EVENT_TYPE_MOTION,
.time = time_msec,
.finger = finger->first,
.pos = finger->second.current,
};

IGestureManager::onTouchMove(touch_event);
}

wf::touch::point_t GestureManager::wlrTouchEventPositionAsPixels(double x, double y) const {
auto area = this->getMonitorArea();
// TODO do I need to add area.x and area.y respectively?
Expand Down
17 changes: 15 additions & 2 deletions src/GestureManager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
#include <hyprland/src/includes.hpp>
#include <vector>
#include <wayfire/touch/touch.hpp>
#include <wayland-server-core.h>

class GestureManager : public IGestureManager {
public:
uint32_t long_press_next_trigger_time;
GestureManager();
~GestureManager();
// @return whether this touch event should be blocked from forwarding to the
// client window/surface
bool onTouchDown(wlr_touch_down_event*);
Expand All @@ -22,15 +25,18 @@ class GestureManager : public IGestureManager {
// client window/surface
bool onTouchMove(wlr_touch_motion_event*);

void onLongPressTimeout(uint32_t time_msec);

protected:
SMonitorArea getMonitorArea() const override;
bool handleGesture(const CompletedGesture& gev) override;
bool handleCompletedGesture(const CompletedGesture& gev) override;
void handleCancelledGesture() override;

private:
std::vector<wlr_surface*> touchedSurfaces;
CMonitor* m_pLastTouchedMonitor;
SMonitorArea m_sMonitorArea;
wl_event_source* long_press_timer;

// for workspace swipe
wf::touch::point_t m_vGestureLastCenter;
Expand All @@ -39,7 +45,14 @@ class GestureManager : public IGestureManager {
void emulateSwipeUpdate(uint32_t time);

wf::touch::point_t wlrTouchEventPositionAsPixels(double x, double y) const;
bool handleWorkspaceSwipe(const CompletedGesture& gev);
bool handleWorkspaceSwipe(const DragGesture& gev);

bool handleDragGesture(const DragGesture& gev) override;
void dragGestureUpdate(const wf::touch::gesture_event_t&) override;
void handleDragGestureEnd(const DragGesture& gev) override;

void updateLongPressTimer(uint32_t current_time, uint32_t delay) override;
void stopLongPressTimer() override;

void sendCancelEventsToWindows() override;
};
Expand Down
Loading

0 comments on commit 7ccb8d3

Please sign in to comment.