├── .clang-format ├── .editorconfig ├── .envrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── checks.yml │ ├── pin-latest-hyprland.yml │ └── update-flake.yml ├── .gitignore ├── .gitmodules ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── VERSION ├── docs └── event_hooks.md ├── examples ├── hyprgrass-pulse │ ├── Debouncer.hpp │ ├── README.md │ ├── globals.hpp │ ├── main.cpp │ ├── meson.build │ ├── pulse.cpp │ └── pulse.hpp └── meson.build ├── flake.lock ├── flake.nix ├── hyprload.toml ├── hyprpm.toml ├── meson.build ├── meson_options.txt ├── nix ├── default.nix ├── hyprgrass-pulse.nix ├── wf-touch.nix └── wf-touch.patch ├── scripts ├── ci │ ├── latest-hyprland-tag │ ├── pin-latest-hyprland │ └── test-pin.sh └── hotreload └── src ├── GestureManager.cpp ├── GestureManager.hpp ├── HyprLogger.hpp ├── TouchVisualizer.cpp ├── TouchVisualizer.hpp ├── VecSet.cpp ├── VecSet.hpp ├── gestures ├── Actions.cpp ├── Actions.hpp ├── CompletedGesture.cpp ├── CompletedGesture.hpp ├── DragGesture.cpp ├── DragGesture.hpp ├── Gestures.cpp ├── Gestures.hpp ├── Logger.hpp ├── Shared.cpp ├── Shared.hpp ├── meson.build └── test │ ├── CoutLogger.hpp │ ├── MockGestureManager.cpp │ ├── MockGestureManager.hpp │ ├── README.md │ ├── meson.build │ └── test.cpp ├── globals.hpp ├── main.cpp ├── meson.build └── version.hpp.in /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | TabWidth: 4 3 | IndentWidth: 4 4 | UseTab: Never 5 | AlignConsecutiveAssignments: true 6 | IndentCaseLabels: true 7 | ColumnLimit: 120 8 | PointerAlignment: Left 9 | AllowShortBlocksOnASingleLine: Empty 10 | AllowShortEnumsOnASingleLine: false 11 | AllowShortFunctionsOnASingleLine: Empty 12 | AllowShortIfStatementsOnASingleLine: Never 13 | AllowShortLoopsOnASingleLine: false 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | charset = utf-8 7 | 8 | # Matches multiple files with brace expansion notation 9 | # Set default charset 10 | [*.{cpp,hpp}] 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [*.toml] 15 | indent_style = space 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Log** 14 | 15 | - For compile errors, run `hyprpm update -f -v --no-shallow` or for nix/home-manager/nixos-rebuild: add the `-L` flag. 16 | 17 | - For runtime crashes, enable logging with `hyprctl keyword debug:disable_logs false` then send the crash report in `.cache/hyprland/hyprlandCrashReport{pid}.txt` 18 | 19 | - For other runtime bugs, if you see something useful in logs after enabling them, also attach it 20 | 21 | ``` 22 | paste log here... 23 | ``` 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Desktop (please complete the following information):** 29 | - OS: [e.g. Arch, nix/nixos] 30 | - output of `hyprctl version` or `Hyprland -v`: 31 | ``` 32 | Hyprland v0.0.0 built from ... 33 | ``` 34 | - output of `hyprctl plugin list`: 35 | ``` 36 | Plugin hyprgrass... 37 | ``` 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: "Nix build test" 2 | on: 3 | pull_request: 4 | jobs: 5 | tests: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: cachix/install-nix-action@v30 10 | with: 11 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 12 | - uses: cachix/cachix-action@v14 13 | with: 14 | name: horriblename 15 | authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 16 | - name: build and test hyprgrass 17 | run: | 18 | nix build .#hyprgrassWithTests \ 19 | --option extra-substituters https://hyprland.cachix.org \ 20 | --option extra-trusted-public-keys hyprland.cachix.org-1:a7pgxzMz7+chwVL3/pzj6jIBMioiJM7ypFP8PwtkuGc= 21 | - name: build hyprgrass-pulse 22 | run: | 23 | nix build .#hyprgrass-pulse \ 24 | --option extra-substituters https://hyprland.cachix.org \ 25 | --option extra-trusted-public-keys hyprland.cachix.org-1:a7pgxzMz7+chwVL3/pzj6jIBMioiJM7ypFP8PwtkuGc= 26 | -------------------------------------------------------------------------------- /.github/workflows/pin-latest-hyprland.yml: -------------------------------------------------------------------------------- 1 | name: "Update hyprpm pin on latest Hyprland release" 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: "0 0 * * 0" # weekly on Sunday 00:00 6 | jobs: 7 | tests: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: cachix/install-nix-action@v30 12 | with: 13 | github_access_token: ${{ secrets.GITHUB_TOKEN }} 14 | extra_nix_config: | 15 | extra-substituters = https://hyprland.cachix.org 16 | extra-trusted-public-keys = hyprland.cachix.org-1:a7pgxzMz7+chwVL3/pzj6jIBMioiJM7ypFP8PwtkuGc= 17 | - name: Update pins 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | run: | 21 | set -eu 22 | set -m # workaround: I get "fg: no job control" even though I don't use any job control 23 | git fetch --depth=1 --tags origin main 24 | 25 | ./scripts/ci/pin-latest-hyprland origin/main 26 | 27 | - name: Create Pull Request 28 | uses: peter-evans/create-pull-request@v7 29 | with: 30 | commit-message: 'ci: automated update of hyprpm.toml' 31 | title: 'Automated PR: Update hyprpm pin' 32 | body: 'This PR was created by a Github Actions workflow' 33 | 34 | -------------------------------------------------------------------------------- /.github/workflows/update-flake.yml: -------------------------------------------------------------------------------- 1 | name: "Update flake inputs" 2 | on: 3 | workflow_dispatch: # allows manual triggering 4 | schedule: 5 | - cron: "0 0 * * 0" # runs weekly on Sunday at 00:00 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: cachix/install-nix-action@v30 13 | with: 14 | github_access_token: ${{ secrets.GH_TOKEN_FOR_UPDATES }} 15 | - uses: cachix/cachix-action@v15 16 | with: 17 | name: horriblename 18 | authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" 19 | - name: Update flake.lock 20 | uses: DeterminateSystems/update-flake-lock@v23 21 | with: 22 | pr-title: "Update flake.lock" # Title of PR to be created 23 | pr-labels: | # Labels to be set on the PR 24 | dependencies 25 | automated 26 | - name: build and test hyprgrass 27 | run: | 28 | nix build .#hyprgrassWithTests \ 29 | --option extra-substituters https://hyprland.cachix.org \ 30 | --option extra-trusted-public-keys hyprland.cachix.org-1:a7pgxzMz7+chwVL3/pzj6jIBMioiJM7ypFP8PwtkuGc= 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | compile_flags.txt 3 | compile_commands.json 4 | .cache/ 5 | .ccls-cache 6 | .ccls 7 | .direnv 8 | result* 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "wf-touch"] 2 | path = subprojects/wf-touch 3 | url = https://github.com/WayfireWM/wf-touch 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Project Structure 2 | 3 | ## Intro 4 | 5 | The two most important files/classes are `IGestureManger` in `src/gestures/Gestures.hpp` and 6 | `GestureManager` in `src/GestureManager.hpp` 7 | 8 | - `IGestureManager` processes touch events (`onTouchDown`) and then emits events when a gesture is triggered/cancelled. 9 | - `GestureManager` implements the event handlers defined in `IGestureManger`. 10 | 11 | **Actions**: `wf::touch::action_t` describe a sequence of touch events and are chained together to implement gestures. Custom actions are defined in `src/gestures/Actions.cpp` 12 | 13 | ## Code Guidelines 14 | 15 | 1. Everything in `src/gestures` should be easily testable - do NOT put Hyprland specific code in this 16 | directory. 17 | 18 | # Commit guidelines 19 | 20 | 1. There are no strict commit message guidelines - I loosely follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). Follow them if possible, it's fine if you don't. 21 | 22 | # A note on wf-touch 23 | 24 | TODO 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Ching Pei Yang 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyprgrass 2 | 3 | Gestures for your touch screen 4 | 5 | > [!WARNING] 6 | > Even though hyprgrass is mostly stable now, there used to be some bugs that render your touch device unusable until you unload the plugin/close Hyprland (https://github.com/horriblename/hyprgrass/issues/27), keep a keyboard in hand the first time you try this. This plugin is still in alpha, expect breaking changes! 7 | 8 | Please open an issue if you find any bugs. Feel free to make a feature request if you have a suggestion. 9 | 10 | ## Features/Roadmap 11 | 12 | - [x] Workspace Swipe 13 | - [x] Custom commands 14 | - [x] Swipe From Edge 15 | - [x] Multi-finger swipe 16 | 17 | ## Installation 18 | 19 | ### Dependencies 20 | 21 | Besides Hyprland (duh), this plugin has the following dependencies: 22 | 23 | ``` 24 | glm 25 | 26 | # build dependencies 27 | meson 28 | ninja 29 | 30 | # extra dependencies for hyprgrass-pulse 31 | libpulseaudio 32 | ``` 33 | 34 | ### Install via hyprpm 35 | 36 | First, install all [dependencies](#dependencies). Then, run these commands: 37 | 38 | ```bash 39 | hyprpm add https://github.com/horriblename/hyprgrass 40 | hyprpm enable hyprgrass 41 | 42 | # optional integration with pulse-audio, see examples/hyprgrass-pulse/README.md 43 | hyprpm enable hyprgrass-pulse 44 | ``` 45 | 46 | You can add `exec-once = hyprpm reload -n` to your hyprland config to have plugins loaded at 47 | startup. -n will make hyprpm send a notification if anything goes wrong (e.g. update needed) 48 | 49 | see [hyprland wiki](https://wiki.hyprland.org/Plugins/Using-Plugins/#hyprpm) for more info 50 | 51 | ### Install via Hyprload 52 | 53 | (hyprload is deprecated, please use hyprpm instead) 54 | 55 | 1. install all [dependencies](#dependencies) 56 | 2. install hyprload by following the instructions 57 | [here](https://github.com/Duckonaut/hyprload#Installing) 58 | 3. put this in `~/.config/hypr/hyprload.toml`: 59 | ``` 60 | plugins = [ 61 | "horriblename/hyprgrass", 62 | ] 63 | ``` 64 | 4. run this command: 65 | 66 | ```bash 67 | # install the plugins 68 | hyprctl dispatch hyprload install 69 | 70 | # load plugins 71 | hyprctl dispatch hyprload load 72 | ``` 73 | 74 | ### Manual compilation 75 | 76 | ```bash 77 | meson setup build 78 | ninja -C build 79 | ``` 80 | 81 | On the `meson setup` step you can pass these options: 82 | - `-Dhyprgrass-pulse=true` to enable building hyprgrass-pulse 83 | 84 | ### Install via nix 85 | 86 | Flakes are highly recommended (because I don't know how to do anything without them) 87 | 88 | Put this in your `flake.nix` file: 89 | 90 | ```nix 91 | { 92 | inputs = { 93 | # ... 94 | hyprland.url = "github:hyprwm/Hyprland"; 95 | hyprgrass = { 96 | url = "github:horriblename/hyprgrass"; 97 | inputs.hyprland.follows = "hyprland"; # IMPORTANT 98 | }; 99 | }; 100 | } 101 | ``` 102 | 103 | and in your home-manager module: 104 | 105 | ```nix 106 | wayland.windowManager.hyprland = { 107 | plugins = [ 108 | inputs.hyprgrass.packages.${pkgs.system}.default 109 | 110 | # optional integration with pulse-audio, see examples/hyprgrass-pulse/README.md 111 | inputs.hyprgrass.packages.${pkgs.system}.hyprgrass-pulse 112 | ]; 113 | }; 114 | ``` 115 | 116 | ## Configuration 117 | 118 | ### Configuration options: 119 | 120 | ``` 121 | plugin { 122 | touch_gestures { 123 | # The default sensitivity is probably too low on tablet screens, 124 | # I recommend turning it up to 4.0 125 | sensitivity = 1.0 126 | 127 | # must be >= 3 128 | workspace_swipe_fingers = 3 129 | 130 | # switching workspaces by swiping from an edge, this is separate from workspace_swipe_fingers 131 | # and can be used at the same time 132 | # possible values: l, r, u, or d 133 | # to disable it set it to anything else 134 | workspace_swipe_edge = d 135 | 136 | # in milliseconds 137 | long_press_delay = 400 138 | 139 | # resize windows by long-pressing on window borders and gaps. 140 | # If general:resize_on_border is enabled, general:extend_border_grab_area is used for floating 141 | # windows 142 | resize_on_border_long_press = true 143 | 144 | # in pixels, the distance from the edge that is considered an edge 145 | edge_margin = 10 146 | 147 | # emulates touchpad swipes when swiping in a direction that does not trigger workspace swipe. 148 | # ONLY triggers when finger count is equal to workspace_swipe_fingers 149 | # 150 | # might be removed in the future in favor of event hooks 151 | emulate_touchpad_swipe = false 152 | 153 | experimental { 154 | # send proper cancel events to windows instead of hacky touch_up events, 155 | # NOT recommended as it crashed a few times, once it's stabilized I'll make it the default 156 | send_cancel = 0 157 | } 158 | } 159 | } 160 | ``` 161 | 162 | #### Other options 163 | 164 | I also recommend that you adjust the settings for the built-in gesture to make it easier to switch workspaces: 165 | 166 | ``` 167 | gestures { 168 | workspace_swipe = true 169 | workspace_swipe_cancel_ratio = 0.15 170 | } 171 | ``` 172 | 173 | ### Custom Commands 174 | 175 | You can also bind gesture events to dispatchers, using hyprgrass-bind keyword. 176 | The syntax is like normal keybinds. 177 | 178 | #### Syntax 179 | 180 | ``` 181 | hyprgrass-bind = , , , 182 | ``` 183 | 184 | where (skip to [examples](#examples) if this is confusing): 185 | 186 | - `gesture_name` is one of: 187 | 1. `swipe::` 188 | - `finger_count` 189 | - `direction` is one of `l`, `r`, `u`, `d`, or `ld`, `rd`, `lu`, `ru` for diagonal directions. 190 | (l, r, u, d stand for left, right, up, down) 191 | 2. `tap:` 192 | 3. `edge::` 193 | - `` is from which edge to start from (l/r/u/d) 194 | - `` is in which direction to swipe (l/r/u/d/lu/ld/ru/rd) 195 | 4. `longpress:` 196 | 197 | #### Examples 198 | 199 | ``` 200 | plugin { 201 | touch_gestures { 202 | # swipe left from right edge 203 | hyprgrass-bind = , edge:r:l, workspace, +1 204 | 205 | # swipe up from bottom edge 206 | hyprgrass-bind = , edge:d:u, exec, firefox 207 | 208 | # swipe down from left edge 209 | hyprgrass-bind = , edge:l:d, exec, pactl set-sink-volume @DEFAULT_SINK@ -4% 210 | 211 | # swipe down with 4 fingers 212 | hyprgrass-bind = , swipe:4:d, killactive 213 | 214 | # swipe diagonally left and down with 3 fingers 215 | # l (or r) must come before d and u 216 | hyprgrass-bind = , swipe:3:ld, exec, foot 217 | 218 | # tap with 3 fingers 219 | hyprgrass-bind = , tap:3, exec, foot 220 | 221 | # longpress can trigger mouse binds: 222 | hyprgrass-bindm = , longpress:2, movewindow 223 | hyprgrass-bindm = , longpress:3, resizewindow 224 | } 225 | } 226 | ``` 227 | 228 | ### Hyprgrass-pulse 229 | 230 | see [./examples/hyprgrass-pulse/README.md](./examples/hyprgrass-pulse/README.md) 231 | 232 | # Other touch screen goodies 233 | 234 | Touch screen related tools I liked. 235 | 236 | On-screen keyboards: 237 | 238 | - [squeekboard](https://gitlab.gnome.org/World/Phosh/squeekboard): has auto show/hide but doesn't 239 | work well with IME (fcitx/IBus etc.) 240 | - [wvkbd](https://github.com/jjsullivan5196/wvkbd): relatively simple keyboard but still has most 241 | important features. 242 | - [fcitx-virtual-keyboard-adapter](https://github.com/horriblename/fcitx-virtualkeyboard-adapter): 243 | NOT a keyboard but an fcitx addon that auto show/hides any on-screen keyboard. 244 | 245 | Miscellanaeious: 246 | 247 | - [iio-hyprland](https://github.com/JeanSchoeller/iio-hyprland/): auto screen rotation 248 | - [Hyprspace](https://github.com/KZDKM/Hyprspace): an overview plugin for hyprland 249 | - [nwg-drawer](https://github.com/nwg-piotr/nwg-drawer): app drawer. Surprisingly, there's not a 250 | lot of those 251 | 252 | # Acknowledgements 253 | 254 | Special thanks to wayfire for the awesome [wf-touch](https://github.com/WayfireWM/wf-touch) library! 255 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | v0.8.2 2 | -------------------------------------------------------------------------------- /docs/event_hooks.md: -------------------------------------------------------------------------------- 1 | # Hyprgrass event hooks 2 | 3 | Hyprgrass exposes event hooks that allow other plugins to integrate with 4 | hyprgrass. These work more or less the same way as Hyprland hook events. You 5 | register them like this: 6 | 7 | ```cpp 8 | void onEdgeBegin(void*, SCallbackInfo& cbinfo, std::any args) { 9 | auto ev = std::any_case>(args); 10 | 11 | // ignore all other events 12 | if ev.first != "edge:l:u" { 13 | return; 14 | } 15 | 16 | // set this to true when you handle a event 17 | // edgeUpdate and edgeEnd events won't trigger otherwise 18 | cbinfo.cancelled = true; 19 | 20 | // do something... 21 | } 22 | 23 | APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { 24 | // note that the hook is dropped when P1 is dropped 25 | static auto P1 = HyprlandAPI::registerCallbackDynamic( 26 | PHANDLE, 27 | "hyprgrass:edgeBegin", 28 | onEdgeBegin 29 | ); 30 | } 31 | ``` 32 | 33 | Examples in the `examples/` directory may be helpful. 34 | 35 | ## Event list 36 | 37 | Table below are the supported events. 38 | 39 | > [!NOTE] `event_name` arguments are as used in hyprgrass-bind, as described in 40 | > the README. Examples: 41 | > 42 | > - `edge:l:u` - Swipe upwards on the left edge 43 | > - `swipe:4:d` - Swipe down with 4 fingers 44 | 45 | > [!NOTE] `position` arguments are the "percentage" position of the fingers in 46 | > the touch device, e.g. 47 | > 48 | > - The top left corner is (0.0, 0.0) 49 | > - The bottom right corner is (1.0, 1.0) 50 | > - The center of the screen is (0.5, 0.5) 51 | 52 | | name | argument types | arguments | Note | 53 | | -------------------- | ---------------------------------- | ---------------------- | --------------------------------------------------------------------- | 54 | | hyprgrass:edgeBegin | `std::pair` | {event_name, position} | set `SCallbackInfo.cancelled = true` after you handle this hook event | 55 | | hyprgrass:edgeUpdate | `Vector2D` | position | | 56 | | hyprgrass:edgeEnd | - | - | | 57 | -------------------------------------------------------------------------------- /examples/hyprgrass-pulse/Debouncer.hpp: -------------------------------------------------------------------------------- 1 | 2 | #include "src/debug/Log.hpp" 3 | #include 4 | #include 5 | #include 6 | 7 | class Debouncer { 8 | public: 9 | using Callback = std::function; 10 | 11 | Debouncer(struct wl_event_loop* loop, int delay_ms, Callback callback) 12 | : loop_(loop), delay_ms_(delay_ms), callback_(callback), timer_source_(nullptr) {} 13 | 14 | ~Debouncer() { 15 | if (timer_source_) { 16 | wl_event_source_remove(timer_source_); 17 | } 18 | } 19 | 20 | void start() { 21 | if (timer_source_) 22 | return; 23 | 24 | // Add a new timer to the event loop 25 | timer_source_ = wl_event_loop_add_timer(loop_, &Debouncer::onTimerEnd, this); 26 | if (timer_source_) { 27 | wl_event_source_timer_update(timer_source_, delay_ms_); 28 | } else { 29 | Debug::log(ERR, "could not create wl timer source"); 30 | } 31 | } 32 | 33 | void disarm() { 34 | if (timer_source_) { 35 | wl_event_source_remove(this->timer_source_); 36 | this->timer_source_ = nullptr; 37 | }; 38 | } 39 | 40 | private: 41 | static int onTimerEnd(void* data) { 42 | auto* debouncer = static_cast(data); 43 | if (debouncer->callback_) { 44 | debouncer->callback_(); 45 | } 46 | 47 | // The timer source is handled, we don't need it anymore 48 | debouncer->timer_source_ = nullptr; 49 | return 0; // Return 0 to indicate success 50 | } 51 | 52 | struct wl_event_loop* loop_; 53 | int delay_ms_; 54 | Callback callback_; 55 | struct wl_event_source* timer_source_; 56 | }; 57 | -------------------------------------------------------------------------------- /examples/hyprgrass-pulse/README.md: -------------------------------------------------------------------------------- 1 | # hyprgrass-pulse 2 | 3 | Control pulse audio volume via hyprgrass edge gestures. 4 | 5 | Haphazardly put together using stolen code from [Waybar](https://github.com/Alexays/Waybar), 6 | licensed under MIT. All credits go to Alexays. 7 | 8 | ## Config 9 | 10 | ``` 11 | plugin { 12 | hyprgrass-pulse { 13 | # Along which edge to trigger the volume changer 14 | # Slide along the edge to adjust volume 15 | # One of: l, r, u, d 16 | edge = l 17 | } 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /examples/hyprgrass-pulse/globals.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | inline HANDLE PHANDLE = nullptr; 6 | -------------------------------------------------------------------------------- /examples/hyprgrass-pulse/main.cpp: -------------------------------------------------------------------------------- 1 | #include "../../src/version.hpp" 2 | #include "Debouncer.hpp" 3 | #include "globals.hpp" 4 | #include "pulse.hpp" 5 | #include "src/SharedDefs.hpp" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | const Hyprgraphics::CColor s_pluginColor = (Hyprgraphics::CColor::SSRGB{0x61 / 255.0f, 0xAF / 255.0f, 0xEF / 255.0f}); 25 | 26 | std::shared_ptr g_pAudioBackend; 27 | std::unique_ptr g_pDebouncer; 28 | struct GlobalState { 29 | Vector2D last_triggered_pos; 30 | Vector2D latest_pos; 31 | }; 32 | std::unique_ptr g_pGlobalState; 33 | 34 | constexpr int ORIGIN_CHAR_INDEX = 5; 35 | constexpr int DIRECTION_CHAR_INDEX = 7; 36 | 37 | void onEdgeBegin(void*, SCallbackInfo& cbinfo, std::any args) { 38 | auto ev = std::any_cast>(args); 39 | 40 | static auto const SWIPE_EDGE = 41 | (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprgrass-pulse:edge") 42 | ->getDataStaticPtr(); 43 | 44 | // we only accept "edge:a:b" 45 | // where a is a single char of the origin side 46 | // and b is a single char of the swipe direction 47 | if (ev.first.size() != 8) { 48 | return; 49 | } 50 | 51 | const char configEdge = *SWIPE_EDGE[0]; 52 | 53 | char origin = ev.first[ORIGIN_CHAR_INDEX]; 54 | char direction = ev.first[DIRECTION_CHAR_INDEX]; 55 | if (origin != configEdge) { 56 | return; 57 | } 58 | 59 | switch (configEdge) { 60 | case 'l': /*fallthrough*/ 61 | case 'r': 62 | if (direction != 'u' && direction != 'd') { 63 | return; 64 | } 65 | break; 66 | 67 | case 'u': /*fallthrough*/ 68 | case 'd': 69 | if (direction != 'l' && direction != 'r') { 70 | return; 71 | } 72 | break; 73 | 74 | default: 75 | return; 76 | } 77 | 78 | g_pGlobalState->latest_pos = ev.second; 79 | g_pGlobalState->last_triggered_pos = ev.second; 80 | 81 | cbinfo.cancelled = true; 82 | } 83 | 84 | void onEdgeUpdate(void*, SCallbackInfo& cbinfo, std::any args) { 85 | auto pos = std::any_cast(args); 86 | 87 | g_pGlobalState->latest_pos = pos; 88 | g_pDebouncer->start(); 89 | } 90 | 91 | void onEdgeEnd(void*, SCallbackInfo& cbinfo, std::any args) { 92 | g_pDebouncer->disarm(); 93 | } 94 | 95 | bool boolXor(bool a, bool b) { 96 | return a != b; 97 | } 98 | 99 | void onDebounceTrigger() { 100 | static auto const SWIPE_EDGE = 101 | (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:hyprgrass-pulse:edge") 102 | ->getDataStaticPtr(); 103 | 104 | if (g_pGlobalState->latest_pos == g_pGlobalState->last_triggered_pos) { 105 | return; 106 | } 107 | 108 | const char configEdge = *SWIPE_EDGE[0]; 109 | 110 | // TODO: make configurable 111 | const double MAX_RANGE = 0.7; // how much percent of the screen to swipe to get from volume 0 to 100 112 | const int PA_MAX_VOLUME = 100; 113 | 114 | bool const vert_swipe = configEdge == 'l' || configEdge == 'r'; 115 | const double last_triggered = 116 | vert_swipe ? g_pGlobalState->last_triggered_pos.y : g_pGlobalState->last_triggered_pos.x; 117 | 118 | const double latest = vert_swipe ? g_pGlobalState->latest_pos.y : g_pGlobalState->latest_pos.x; 119 | 120 | double delta = std::abs(latest - last_triggered); 121 | ChangeType change = boolXor(vert_swipe, latest >= last_triggered) ? ChangeType::Increase : ChangeType::Decrease; 122 | 123 | double steps = PA_MAX_VOLUME * (delta / MAX_RANGE); 124 | 125 | g_pAudioBackend->changeVolume(change, steps, PA_MAX_VOLUME); 126 | 127 | g_pGlobalState->last_triggered_pos = g_pGlobalState->latest_pos; 128 | } 129 | 130 | // Do NOT change this function. 131 | APICALL EXPORT std::string PLUGIN_API_VERSION() { 132 | return HYPRLAND_API_VERSION; 133 | } 134 | 135 | APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { 136 | PHANDLE = handle; 137 | 138 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:hyprgrass-pulse:edge", Hyprlang::CConfigValue((Hyprlang::STRING) "l")); 139 | 140 | const auto hlTargetVersion = GIT_COMMIT_HASH; 141 | const auto hlVersion = HyprlandAPI::getHyprlandVersion(PHANDLE); 142 | 143 | if (hlVersion.hash != hlTargetVersion) { 144 | HyprlandAPI::addNotification(PHANDLE, "Mismatched Hyprland version! check logs for details", 145 | CHyprColor(s_pluginColor, 1.0), 5000); 146 | Debug::log(ERR, "[hyprgrass] version mismatch!"); 147 | Debug::log(ERR, "[hyprgrass] | hyprgrass was built against: {}", hlTargetVersion); 148 | Debug::log(ERR, "[hyprgrass] | actual hyprland version: {}", hlVersion.hash); 149 | } 150 | 151 | g_pGlobalState = std::make_unique(); 152 | g_pAudioBackend = AudioBackend::getInstance(); 153 | g_pDebouncer = std::make_unique(g_pCompositor->m_wlEventLoop, 16, onDebounceTrigger); 154 | 155 | static auto P1 = HyprlandAPI::registerCallbackDynamic(PHANDLE, "hyprgrass:edgeBegin", onEdgeBegin); 156 | static auto P2 = HyprlandAPI::registerCallbackDynamic(PHANDLE, "hyprgrass:edgeUpdate", onEdgeUpdate); 157 | static auto P3 = HyprlandAPI::registerCallbackDynamic(PHANDLE, "hyprgrass:edgeEnd", onEdgeEnd); 158 | 159 | if (!P1 || !P2 || !P3) { 160 | Debug::log(LOG, "[hyprgrass pulse] something went wrong: could not register hooks"); 161 | } 162 | 163 | HyprlandAPI::reloadConfig(); 164 | 165 | return {"hyprgrass-pulse", "Hyprgrass pulseaudio extension", "horriblename", HYPRGRASS_VERSION}; 166 | } 167 | 168 | APICALL EXPORT void PLUGIN_EXIT() { 169 | g_pDebouncer.reset(); 170 | g_pAudioBackend.reset(); 171 | g_pGlobalState.reset(); 172 | } 173 | -------------------------------------------------------------------------------- /examples/hyprgrass-pulse/meson.build: -------------------------------------------------------------------------------- 1 | if get_option('hyprgrass-pulse') 2 | pulse = dependency('libpulse') 3 | 4 | shared_module( 5 | 'hyprgrass-pulse', 6 | 'main.cpp', 7 | 'pulse.cpp', 8 | dependencies: [pulse, hyprland_headers, hyprland_deps], 9 | install: true 10 | ) 11 | endif 12 | -------------------------------------------------------------------------------- /examples/hyprgrass-pulse/pulse.cpp: -------------------------------------------------------------------------------- 1 | #include "pulse.hpp" 2 | #include "pulse/thread-mainloop.h" 3 | #include "src/debug/Log.hpp" 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | AudioBackend::AudioBackend(std::function on_updated_cb, private_constructor_tag tag) 17 | : mainloop_(nullptr), mainloop_api_(nullptr), context_(nullptr), volume_(0), muted_(false), source_volume_(0), 18 | source_muted_(false), on_updated_cb_(std::move(on_updated_cb)) { 19 | mainloop_ = pa_threaded_mainloop_new(); 20 | if (mainloop_ == nullptr) { 21 | throw std::runtime_error("pa_mainloop_new() failed."); 22 | } 23 | pa_threaded_mainloop_lock(mainloop_); 24 | mainloop_api_ = pa_threaded_mainloop_get_api(mainloop_); 25 | connectContext(); 26 | if (pa_threaded_mainloop_start(mainloop_) < 0) { 27 | throw std::runtime_error("pa_mainloop_run() failed."); 28 | } 29 | pa_threaded_mainloop_unlock(mainloop_); 30 | } 31 | 32 | AudioBackend::~AudioBackend() { 33 | if (context_ != nullptr) { 34 | pa_context_disconnect(context_); 35 | } 36 | 37 | if (mainloop_ != nullptr) { 38 | mainloop_api_->quit(mainloop_api_, 0); 39 | pa_threaded_mainloop_stop(mainloop_); 40 | pa_threaded_mainloop_free(mainloop_); 41 | } 42 | } 43 | 44 | std::shared_ptr AudioBackend::getInstance(std::function on_updated_cb) { 45 | private_constructor_tag tag; 46 | return std::make_shared(on_updated_cb, tag); 47 | } 48 | 49 | void AudioBackend::connectContext() { 50 | context_ = pa_context_new(mainloop_api_, "waybar"); 51 | if (context_ == nullptr) { 52 | throw std::runtime_error("pa_context_new() failed."); 53 | } 54 | pa_context_set_state_callback(context_, contextStateCb, this); 55 | if (pa_context_connect(context_, nullptr, PA_CONTEXT_NOFAIL, nullptr) < 0) { 56 | Debug::log(ERR, "pa_context_connect() failed: {}", pa_strerror(pa_context_errno(context_))); 57 | } 58 | } 59 | 60 | void AudioBackend::contextStateCb(pa_context* c, void* data) { 61 | auto* backend = static_cast(data); 62 | switch (pa_context_get_state(c)) { 63 | case PA_CONTEXT_TERMINATED: 64 | backend->mainloop_api_->quit(backend->mainloop_api_, 0); 65 | break; 66 | case PA_CONTEXT_READY: 67 | pa_context_get_server_info(c, serverInfoCb, data); 68 | pa_context_set_subscribe_callback(c, subscribeCb, data); 69 | pa_context_subscribe( 70 | c, 71 | static_cast( 72 | static_cast(PA_SUBSCRIPTION_MASK_SERVER) | static_cast(PA_SUBSCRIPTION_MASK_SINK) | 73 | static_cast(PA_SUBSCRIPTION_MASK_SINK_INPUT) | static_cast(PA_SUBSCRIPTION_MASK_SOURCE) | 74 | static_cast(PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT)), 75 | nullptr, nullptr); 76 | break; 77 | case PA_CONTEXT_FAILED: 78 | // When pulseaudio server restarts, the connection is "failed". Try to reconnect. 79 | // pa_threaded_mainloop_lock is already acquired in callback threads. 80 | // So there is no need to lock it again. 81 | if (backend->context_ != nullptr) { 82 | pa_context_disconnect(backend->context_); 83 | } 84 | backend->connectContext(); 85 | break; 86 | case PA_CONTEXT_CONNECTING: 87 | case PA_CONTEXT_AUTHORIZING: 88 | case PA_CONTEXT_SETTING_NAME: 89 | default: 90 | break; 91 | } 92 | } 93 | 94 | /* 95 | * Called when an event we subscribed to occurs. 96 | */ 97 | void AudioBackend::subscribeCb(pa_context* context, pa_subscription_event_type_t type, uint32_t idx, void* data) { 98 | unsigned facility = type & PA_SUBSCRIPTION_EVENT_FACILITY_MASK; 99 | unsigned operation = type & PA_SUBSCRIPTION_EVENT_TYPE_MASK; 100 | if (operation != PA_SUBSCRIPTION_EVENT_CHANGE) { 101 | return; 102 | } 103 | if (facility == PA_SUBSCRIPTION_EVENT_SERVER) { 104 | pa_context_get_server_info(context, serverInfoCb, data); 105 | } else if (facility == PA_SUBSCRIPTION_EVENT_SINK) { 106 | pa_context_get_sink_info_by_index(context, idx, sinkInfoCb, data); 107 | } else if (facility == PA_SUBSCRIPTION_EVENT_SINK_INPUT) { 108 | pa_context_get_sink_info_list(context, sinkInfoCb, data); 109 | } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE) { 110 | pa_context_get_source_info_by_index(context, idx, sourceInfoCb, data); 111 | } else if (facility == PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT) { 112 | pa_context_get_source_info_list(context, sourceInfoCb, data); 113 | } 114 | } 115 | 116 | /* 117 | * Called in response to a volume change request 118 | */ 119 | void AudioBackend::volumeModifyCb(pa_context* c, int success, void* data) { 120 | auto* backend = static_cast(data); 121 | if (success != 0) { 122 | pa_context_get_sink_info_by_index(backend->context_, backend->sink_idx_, sinkInfoCb, data); 123 | } 124 | } 125 | 126 | /* 127 | * Called when the requested sink information is ready. 128 | */ 129 | void AudioBackend::sinkInfoCb(pa_context* /*context*/, const pa_sink_info* i, int /*eol*/, void* data) { 130 | if (i == nullptr) 131 | return; 132 | 133 | auto running = i->state == PA_SINK_RUNNING; 134 | auto idle = i->state == PA_SINK_IDLE; 135 | Debug::log(TRACE, "Sink name {} Running:[{}] Idle:[{}]", i->name, running, idle); 136 | 137 | auto* backend = static_cast(data); 138 | 139 | if (!backend->ignored_sinks_.empty()) { 140 | for (const auto& ignored_sink : backend->ignored_sinks_) { 141 | if (ignored_sink == i->description) { 142 | if (i->name == backend->current_sink_name_) { 143 | // If the current sink happens to be ignored it is never considered running 144 | // so it will be replaced with another sink. 145 | backend->current_sink_running_ = false; 146 | } 147 | 148 | return; 149 | } 150 | } 151 | } 152 | 153 | backend->default_sink_running_ = 154 | backend->default_sink_name == i->name && (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); 155 | 156 | if (i->name != backend->default_sink_name && !backend->default_sink_running_) { 157 | return; 158 | } 159 | 160 | if (backend->current_sink_name_ == i->name) { 161 | backend->current_sink_running_ = (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE); 162 | } 163 | 164 | if (!backend->current_sink_running_ && (i->state == PA_SINK_RUNNING || i->state == PA_SINK_IDLE)) { 165 | backend->current_sink_name_ = i->name; 166 | backend->current_sink_running_ = true; 167 | } 168 | 169 | if (backend->current_sink_name_ == i->name) { 170 | backend->pa_volume_ = i->volume; 171 | float volume = static_cast(pa_cvolume_avg(&(backend->pa_volume_))) / float{PA_VOLUME_NORM}; 172 | backend->sink_idx_ = i->index; 173 | backend->volume_ = std::round(volume * 100.0F); 174 | backend->muted_ = i->mute != 0; 175 | backend->desc_ = i->description; 176 | backend->monitor_ = i->monitor_source_name; 177 | backend->port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; 178 | if (const auto* ff = pa_proplist_gets(i->proplist, PA_PROP_DEVICE_FORM_FACTOR)) { 179 | backend->form_factor_ = ff; 180 | } else { 181 | backend->form_factor_ = ""; 182 | } 183 | backend->on_updated_cb_(); 184 | } 185 | } 186 | 187 | /* 188 | * Called when the requested source information is ready. 189 | */ 190 | void AudioBackend::sourceInfoCb(pa_context* /*context*/, const pa_source_info* i, int /*eol*/, void* data) { 191 | auto* backend = static_cast(data); 192 | if (i != nullptr && backend->default_source_name_ == i->name) { 193 | auto source_volume = static_cast(pa_cvolume_avg(&(i->volume))) / float{PA_VOLUME_NORM}; 194 | backend->source_volume_ = std::round(source_volume * 100.0F); 195 | backend->source_idx_ = i->index; 196 | backend->source_muted_ = i->mute != 0; 197 | backend->source_desc_ = i->description; 198 | backend->source_port_name_ = i->active_port != nullptr ? i->active_port->name : "Unknown"; 199 | backend->on_updated_cb_(); 200 | } 201 | } 202 | 203 | /* 204 | * Called when the requested information on the server is ready. This is 205 | * used to find the default PulseAudio sink. 206 | */ 207 | void AudioBackend::serverInfoCb(pa_context* context, const pa_server_info* i, void* data) { 208 | auto* backend = static_cast(data); 209 | backend->current_sink_name_ = i->default_sink_name; 210 | backend->default_sink_name = i->default_sink_name; 211 | backend->default_source_name_ = i->default_source_name; 212 | 213 | pa_context_get_sink_info_list(context, sinkInfoCb, data); 214 | pa_context_get_source_info_list(context, sourceInfoCb, data); 215 | } 216 | 217 | void AudioBackend::changeVolume(uint16_t volume, uint16_t min_volume, uint16_t max_volume) { 218 | double volume_tick = static_cast(PA_VOLUME_NORM) / 100; 219 | pa_cvolume pa_volume = pa_volume_; 220 | 221 | volume = std::clamp(volume, min_volume, max_volume); 222 | pa_cvolume_set(&pa_volume, pa_volume_.channels, volume * volume_tick); 223 | 224 | pa_threaded_mainloop_lock(mainloop_); 225 | pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); 226 | pa_threaded_mainloop_unlock(mainloop_); 227 | } 228 | 229 | void AudioBackend::changeVolume(ChangeType change_type, double step, uint16_t max_volume) { 230 | double volume_tick = static_cast(PA_VOLUME_NORM) / 100; 231 | pa_volume_t change = volume_tick; 232 | pa_cvolume pa_volume = pa_volume_; 233 | 234 | max_volume = std::min(max_volume, static_cast(PA_VOLUME_UI_MAX)); 235 | 236 | if (change_type == ChangeType::Increase) { 237 | if (volume_ < max_volume) { 238 | if (volume_ + step > max_volume) { 239 | change = round((max_volume - volume_) * volume_tick); 240 | } else { 241 | change = round(step * volume_tick); 242 | } 243 | pa_cvolume_inc(&pa_volume, change); 244 | } 245 | } else if (change_type == ChangeType::Decrease) { 246 | if (volume_ > 0) { 247 | if (volume_ - step < 0) { 248 | change = round(volume_ * volume_tick); 249 | } else { 250 | change = round(step * volume_tick); 251 | } 252 | pa_cvolume_dec(&pa_volume, change); 253 | } 254 | } 255 | pa_threaded_mainloop_lock(mainloop_); 256 | pa_context_set_sink_volume_by_index(context_, sink_idx_, &pa_volume, volumeModifyCb, this); 257 | pa_threaded_mainloop_unlock(mainloop_); 258 | } 259 | -------------------------------------------------------------------------------- /examples/hyprgrass-pulse/pulse.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | enum class ChangeType { 13 | Increase, 14 | Decrease, 15 | }; 16 | 17 | class AudioBackend { 18 | private: 19 | static void subscribeCb(pa_context*, pa_subscription_event_type_t, uint32_t, void*); 20 | static void contextStateCb(pa_context*, void*); 21 | static void sinkInfoCb(pa_context*, const pa_sink_info*, int, void*); 22 | static void sourceInfoCb(pa_context*, const pa_source_info* i, int, void* data); 23 | static void serverInfoCb(pa_context*, const pa_server_info*, void*); 24 | static void volumeModifyCb(pa_context*, int, void*); 25 | void connectContext(); 26 | 27 | pa_threaded_mainloop* mainloop_; 28 | pa_mainloop_api* mainloop_api_; 29 | pa_context* context_; 30 | pa_cvolume pa_volume_; 31 | 32 | // SINK 33 | uint32_t sink_idx_{0}; 34 | uint16_t volume_; 35 | bool muted_; 36 | std::string port_name_; 37 | std::string form_factor_; 38 | std::string desc_; 39 | std::string monitor_; 40 | std::string current_sink_name_; 41 | std::string default_sink_name; 42 | bool default_sink_running_; 43 | bool current_sink_running_; 44 | // SOURCE 45 | uint32_t source_idx_{0}; 46 | uint16_t source_volume_; 47 | bool source_muted_; 48 | std::string source_port_name_; 49 | std::string source_desc_; 50 | std::string default_source_name_; 51 | 52 | std::vector ignored_sinks_; 53 | 54 | std::function on_updated_cb_ = []() {}; 55 | 56 | /* Hack to keep constructor inaccessible but still public. 57 | * This is required to be able to use std::make_shared. 58 | * It is important to keep this class only accessible via a reference-counted 59 | * pointer because the destructor will manually free memory, and this could be 60 | * a problem with C++20's copy and move semantics. 61 | */ 62 | struct private_constructor_tag {}; 63 | 64 | public: 65 | static std::shared_ptr getInstance(std::function on_updated_cb = []() {}); 66 | 67 | AudioBackend(std::function on_updated_cb, private_constructor_tag tag); 68 | ~AudioBackend(); 69 | 70 | void changeVolume(uint16_t volume, uint16_t min_volume = 0, uint16_t max_volume = 100); 71 | void changeVolume(ChangeType change_type, double step = 1, uint16_t max_volume = 100); 72 | }; 73 | -------------------------------------------------------------------------------- /examples/meson.build: -------------------------------------------------------------------------------- 1 | subdir('hyprgrass-pulse') 2 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "aquamarine": { 4 | "inputs": { 5 | "hyprutils": [ 6 | "hyprland", 7 | "hyprutils" 8 | ], 9 | "hyprwayland-scanner": [ 10 | "hyprland", 11 | "hyprwayland-scanner" 12 | ], 13 | "nixpkgs": [ 14 | "hyprland", 15 | "nixpkgs" 16 | ], 17 | "systems": [ 18 | "hyprland", 19 | "systems" 20 | ] 21 | }, 22 | "locked": { 23 | "lastModified": 1747864449, 24 | "narHash": "sha256-PIjVAWghZhr3L0EFM2UObhX84UQxIACbON0IC0zzSKA=", 25 | "owner": "hyprwm", 26 | "repo": "aquamarine", 27 | "rev": "389372c5f4dc1ac0e7645ed29a35fd6d71672ef5", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "hyprwm", 32 | "repo": "aquamarine", 33 | "type": "github" 34 | } 35 | }, 36 | "flake-compat": { 37 | "flake": false, 38 | "locked": { 39 | "lastModified": 1696426674, 40 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 41 | "owner": "edolstra", 42 | "repo": "flake-compat", 43 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "edolstra", 48 | "repo": "flake-compat", 49 | "type": "github" 50 | } 51 | }, 52 | "gitignore": { 53 | "inputs": { 54 | "nixpkgs": [ 55 | "hyprland", 56 | "pre-commit-hooks", 57 | "nixpkgs" 58 | ] 59 | }, 60 | "locked": { 61 | "lastModified": 1709087332, 62 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 63 | "owner": "hercules-ci", 64 | "repo": "gitignore.nix", 65 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 66 | "type": "github" 67 | }, 68 | "original": { 69 | "owner": "hercules-ci", 70 | "repo": "gitignore.nix", 71 | "type": "github" 72 | } 73 | }, 74 | "hyprcursor": { 75 | "inputs": { 76 | "hyprlang": [ 77 | "hyprland", 78 | "hyprlang" 79 | ], 80 | "nixpkgs": [ 81 | "hyprland", 82 | "nixpkgs" 83 | ], 84 | "systems": [ 85 | "hyprland", 86 | "systems" 87 | ] 88 | }, 89 | "locked": { 90 | "lastModified": 1745948457, 91 | "narHash": "sha256-lzTV10FJTCGNtMdgW5YAhCAqezeAzKOd/97HbQK8GTU=", 92 | "owner": "hyprwm", 93 | "repo": "hyprcursor", 94 | "rev": "ac903e80b33ba6a88df83d02232483d99f327573", 95 | "type": "github" 96 | }, 97 | "original": { 98 | "owner": "hyprwm", 99 | "repo": "hyprcursor", 100 | "type": "github" 101 | } 102 | }, 103 | "hyprgraphics": { 104 | "inputs": { 105 | "hyprutils": [ 106 | "hyprland", 107 | "hyprutils" 108 | ], 109 | "nixpkgs": [ 110 | "hyprland", 111 | "nixpkgs" 112 | ], 113 | "systems": [ 114 | "hyprland", 115 | "systems" 116 | ] 117 | }, 118 | "locked": { 119 | "lastModified": 1745015490, 120 | "narHash": "sha256-apEJ9zoSzmslhJ2vOKFcXTMZLUFYzh1ghfB6Rbw3Low=", 121 | "owner": "hyprwm", 122 | "repo": "hyprgraphics", 123 | "rev": "60754910946b4e2dc1377b967b7156cb989c5873", 124 | "type": "github" 125 | }, 126 | "original": { 127 | "owner": "hyprwm", 128 | "repo": "hyprgraphics", 129 | "type": "github" 130 | } 131 | }, 132 | "hyprland": { 133 | "inputs": { 134 | "aquamarine": "aquamarine", 135 | "hyprcursor": "hyprcursor", 136 | "hyprgraphics": "hyprgraphics", 137 | "hyprland-protocols": "hyprland-protocols", 138 | "hyprland-qtutils": "hyprland-qtutils", 139 | "hyprlang": "hyprlang", 140 | "hyprutils": "hyprutils", 141 | "hyprwayland-scanner": "hyprwayland-scanner", 142 | "nixpkgs": "nixpkgs", 143 | "pre-commit-hooks": "pre-commit-hooks", 144 | "systems": "systems", 145 | "xdph": "xdph" 146 | }, 147 | "locked": { 148 | "lastModified": 1748717390, 149 | "narHash": "sha256-UrGKyTng69G6QYqJVWpzl83l2aRgtYjBXbo7T+SSF58=", 150 | "owner": "hyprwm", 151 | "repo": "Hyprland", 152 | "rev": "69c2b2926e128f1fd09080aed43944987a42026f", 153 | "type": "github" 154 | }, 155 | "original": { 156 | "owner": "hyprwm", 157 | "repo": "Hyprland", 158 | "type": "github" 159 | } 160 | }, 161 | "hyprland-protocols": { 162 | "inputs": { 163 | "nixpkgs": [ 164 | "hyprland", 165 | "nixpkgs" 166 | ], 167 | "systems": [ 168 | "hyprland", 169 | "systems" 170 | ] 171 | }, 172 | "locked": { 173 | "lastModified": 1743714874, 174 | "narHash": "sha256-yt8F7NhMFCFHUHy/lNjH/pjZyIDFNk52Q4tivQ31WFo=", 175 | "owner": "hyprwm", 176 | "repo": "hyprland-protocols", 177 | "rev": "3a5c2bda1c1a4e55cc1330c782547695a93f05b2", 178 | "type": "github" 179 | }, 180 | "original": { 181 | "owner": "hyprwm", 182 | "repo": "hyprland-protocols", 183 | "type": "github" 184 | } 185 | }, 186 | "hyprland-qt-support": { 187 | "inputs": { 188 | "hyprlang": [ 189 | "hyprland", 190 | "hyprland-qtutils", 191 | "hyprlang" 192 | ], 193 | "nixpkgs": [ 194 | "hyprland", 195 | "hyprland-qtutils", 196 | "nixpkgs" 197 | ], 198 | "systems": [ 199 | "hyprland", 200 | "hyprland-qtutils", 201 | "systems" 202 | ] 203 | }, 204 | "locked": { 205 | "lastModified": 1737634706, 206 | "narHash": "sha256-nGCibkfsXz7ARx5R+SnisRtMq21IQIhazp6viBU8I/A=", 207 | "owner": "hyprwm", 208 | "repo": "hyprland-qt-support", 209 | "rev": "8810df502cdee755993cb803eba7b23f189db795", 210 | "type": "github" 211 | }, 212 | "original": { 213 | "owner": "hyprwm", 214 | "repo": "hyprland-qt-support", 215 | "type": "github" 216 | } 217 | }, 218 | "hyprland-qtutils": { 219 | "inputs": { 220 | "hyprland-qt-support": "hyprland-qt-support", 221 | "hyprlang": [ 222 | "hyprland", 223 | "hyprlang" 224 | ], 225 | "hyprutils": [ 226 | "hyprland", 227 | "hyprland-qtutils", 228 | "hyprlang", 229 | "hyprutils" 230 | ], 231 | "nixpkgs": [ 232 | "hyprland", 233 | "nixpkgs" 234 | ], 235 | "systems": [ 236 | "hyprland", 237 | "systems" 238 | ] 239 | }, 240 | "locked": { 241 | "lastModified": 1745951494, 242 | "narHash": "sha256-2dModE32doiyQMmd6EDAQeZnz+5LOs6KXyE0qX76WIg=", 243 | "owner": "hyprwm", 244 | "repo": "hyprland-qtutils", 245 | "rev": "4be1d324faf8d6e82c2be9f8510d299984dfdd2e", 246 | "type": "github" 247 | }, 248 | "original": { 249 | "owner": "hyprwm", 250 | "repo": "hyprland-qtutils", 251 | "type": "github" 252 | } 253 | }, 254 | "hyprlang": { 255 | "inputs": { 256 | "hyprutils": [ 257 | "hyprland", 258 | "hyprutils" 259 | ], 260 | "nixpkgs": [ 261 | "hyprland", 262 | "nixpkgs" 263 | ], 264 | "systems": [ 265 | "hyprland", 266 | "systems" 267 | ] 268 | }, 269 | "locked": { 270 | "lastModified": 1747484975, 271 | "narHash": "sha256-+LAQ81HBwG0lwshHlWe0kfWg4KcChIPpnwtnwqmnoEU=", 272 | "owner": "hyprwm", 273 | "repo": "hyprlang", 274 | "rev": "163c83b3db48a17c113729c220a60b94596c9291", 275 | "type": "github" 276 | }, 277 | "original": { 278 | "owner": "hyprwm", 279 | "repo": "hyprlang", 280 | "type": "github" 281 | } 282 | }, 283 | "hyprutils": { 284 | "inputs": { 285 | "nixpkgs": [ 286 | "hyprland", 287 | "nixpkgs" 288 | ], 289 | "systems": [ 290 | "hyprland", 291 | "systems" 292 | ] 293 | }, 294 | "locked": { 295 | "lastModified": 1746635225, 296 | "narHash": "sha256-W9G9bb0zRYDBRseHbVez0J8qVpD5QbizX67H/vsudhM=", 297 | "owner": "hyprwm", 298 | "repo": "hyprutils", 299 | "rev": "674ea57373f08b7609ce93baff131117a0dfe70d", 300 | "type": "github" 301 | }, 302 | "original": { 303 | "owner": "hyprwm", 304 | "repo": "hyprutils", 305 | "type": "github" 306 | } 307 | }, 308 | "hyprwayland-scanner": { 309 | "inputs": { 310 | "nixpkgs": [ 311 | "hyprland", 312 | "nixpkgs" 313 | ], 314 | "systems": [ 315 | "hyprland", 316 | "systems" 317 | ] 318 | }, 319 | "locked": { 320 | "lastModified": 1747584298, 321 | "narHash": "sha256-PH9qZqWLHvSBQiUnA0NzAyQA3tu2no2z8kz0ZeHWj4w=", 322 | "owner": "hyprwm", 323 | "repo": "hyprwayland-scanner", 324 | "rev": "e511882b9c2e1d7a75d45d8fddd2160daeafcbc3", 325 | "type": "github" 326 | }, 327 | "original": { 328 | "owner": "hyprwm", 329 | "repo": "hyprwayland-scanner", 330 | "type": "github" 331 | } 332 | }, 333 | "nixpkgs": { 334 | "locked": { 335 | "lastModified": 1748460289, 336 | "narHash": "sha256-7doLyJBzCllvqX4gszYtmZUToxKvMUrg45EUWaUYmBg=", 337 | "owner": "NixOS", 338 | "repo": "nixpkgs", 339 | "rev": "96ec055edbe5ee227f28cdbc3f1ddf1df5965102", 340 | "type": "github" 341 | }, 342 | "original": { 343 | "owner": "NixOS", 344 | "ref": "nixos-unstable", 345 | "repo": "nixpkgs", 346 | "type": "github" 347 | } 348 | }, 349 | "pre-commit-hooks": { 350 | "inputs": { 351 | "flake-compat": "flake-compat", 352 | "gitignore": "gitignore", 353 | "nixpkgs": [ 354 | "hyprland", 355 | "nixpkgs" 356 | ] 357 | }, 358 | "locked": { 359 | "lastModified": 1747372754, 360 | "narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=", 361 | "owner": "cachix", 362 | "repo": "git-hooks.nix", 363 | "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46", 364 | "type": "github" 365 | }, 366 | "original": { 367 | "owner": "cachix", 368 | "repo": "git-hooks.nix", 369 | "type": "github" 370 | } 371 | }, 372 | "root": { 373 | "inputs": { 374 | "hyprland": "hyprland", 375 | "nixpkgs": [ 376 | "hyprland", 377 | "nixpkgs" 378 | ] 379 | } 380 | }, 381 | "systems": { 382 | "locked": { 383 | "lastModified": 1689347949, 384 | "narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=", 385 | "owner": "nix-systems", 386 | "repo": "default-linux", 387 | "rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68", 388 | "type": "github" 389 | }, 390 | "original": { 391 | "owner": "nix-systems", 392 | "repo": "default-linux", 393 | "type": "github" 394 | } 395 | }, 396 | "xdph": { 397 | "inputs": { 398 | "hyprland-protocols": [ 399 | "hyprland", 400 | "hyprland-protocols" 401 | ], 402 | "hyprlang": [ 403 | "hyprland", 404 | "hyprlang" 405 | ], 406 | "hyprutils": [ 407 | "hyprland", 408 | "hyprutils" 409 | ], 410 | "hyprwayland-scanner": [ 411 | "hyprland", 412 | "hyprwayland-scanner" 413 | ], 414 | "nixpkgs": [ 415 | "hyprland", 416 | "nixpkgs" 417 | ], 418 | "systems": [ 419 | "hyprland", 420 | "systems" 421 | ] 422 | }, 423 | "locked": { 424 | "lastModified": 1745871725, 425 | "narHash": "sha256-M24SNc2flblWGXFkGQfqSlEOzAGZnMc9QG3GH4K/KbE=", 426 | "owner": "hyprwm", 427 | "repo": "xdg-desktop-portal-hyprland", 428 | "rev": "76bbf1a6b1378e4ab5230bad00ad04bc287c969e", 429 | "type": "github" 430 | }, 431 | "original": { 432 | "owner": "hyprwm", 433 | "repo": "xdg-desktop-portal-hyprland", 434 | "type": "github" 435 | } 436 | } 437 | }, 438 | "root": "root", 439 | "version": 7 440 | } 441 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Hyprland plugin for touch gestures"; 3 | 4 | inputs = { 5 | hyprland.url = "github:hyprwm/Hyprland"; 6 | nixpkgs.follows = "hyprland/nixpkgs"; 7 | }; 8 | 9 | outputs = { 10 | self, 11 | nixpkgs, 12 | hyprland, 13 | ... 14 | }: let 15 | withPkgsFor = fn: 16 | nixpkgs.lib.genAttrs (builtins.attrNames hyprland.packages) (system: 17 | fn system (import nixpkgs { 18 | inherit system; 19 | overlays = [ 20 | hyprland.overlays.hyprland-packages 21 | self.overlays.default 22 | ]; 23 | })); 24 | in { 25 | packages = withPkgsFor (system: pkgs: rec { 26 | inherit (pkgs) wf-touch; 27 | inherit (pkgs.hyprlandPlugins) hyprgrass hyprgrass-pulse; 28 | 29 | default = hyprgrass; 30 | hyprgrassWithTests = hyprgrass.override {runTests = true;}; 31 | }); 32 | 33 | overlays = { 34 | default = self.overlays.hyprgrass; 35 | 36 | hyprgrass = final: prev: let 37 | tag = final.lib.replaceStrings ["\n" "v"] ["" ""] (builtins.readFile ./VERSION); 38 | commit = self.shortRev or "dirty"; 39 | in { 40 | wf-touch = final.callPackage ./nix/wf-touch.nix {}; 41 | 42 | hyprlandPlugins = 43 | (prev.hyprlandPlugins or {}) 44 | // { 45 | hyprgrass = final.callPackage ./nix/default.nix {inherit tag commit;}; 46 | hyprgrass-pulse = final.callPackage ./nix/hyprgrass-pulse.nix {inherit tag commit;}; 47 | }; 48 | }; 49 | }; 50 | 51 | devShells = withPkgsFor (system: pkgs: { 52 | default = pkgs.mkShell.override {inherit (pkgs.hyprland) stdenv;} { 53 | shellHook = '' 54 | meson setup build -Dbuildtype=debug -Dhyprgrass-pulse=true --reconfigure 55 | sed -e 's/c++23/c++2b/g' ./build/compile_commands.json > ./compile_commands.json 56 | ''; 57 | name = "hyprgrass-shell"; 58 | nativeBuildInputs = with pkgs; [meson pkg-config ninja]; 59 | buildInputs = [pkgs.hyprland pkgs.libpulseaudio]; 60 | inputsFrom = [ 61 | pkgs.hyprland 62 | pkgs.hyprlandPlugins.hyprgrass 63 | ]; 64 | }; 65 | }); 66 | 67 | formatter = withPkgsFor (_system: pkgs: pkgs.alejandra); 68 | }; 69 | } 70 | -------------------------------------------------------------------------------- /hyprload.toml: -------------------------------------------------------------------------------- 1 | [hyprgrass] 2 | description = "Touch gestures" 3 | author = "horriblename" 4 | 5 | [hyprgrass.build] 6 | output = "build/src/libhyprgrass.so" 7 | steps = [ 8 | "meson setup build", 9 | "ninja -C build", 10 | ] 11 | 12 | [hyprgrass-pulse] 13 | description = "Hyprgras extension to control audio volume in pulse-audio" 14 | author = "horriblename" 15 | 16 | [hyprgrass-pulse] 17 | output = "build/examples/hyprgrass-pulse/libhyprgrass-pulse.so" 18 | steps = [ 19 | "meson setup build -Dhyprgrass=false -Dhyprgrass-pulse=true", 20 | "ninja -C build", 21 | ] 22 | -------------------------------------------------------------------------------- /hyprpm.toml: -------------------------------------------------------------------------------- 1 | [repository] 2 | name = "hyprgrass" 3 | authors = ["horriblename"] 4 | commit_pins = [ 5 | # Hyprland commit , hyprgrass commit 6 | ["e93fbd7c4f991cb8ef03e433ccc4d43587923e15", "091d0e9a9877d08d5d4f51eb71e255b8c78ffd89"], 7 | ["fe7b748eb668136dd0558b7c8279bfcd7ab4d759", "091d0e9a9877d08d5d4f51eb71e255b8c78ffd89"], 8 | ["cba1ade848feac44b2eda677503900639581c3f4", "f888dab948219197e2870cfd261b6f87690484a7"], # v0.40.0 9 | ["ea2501d4556f84d3de86a4ae2f4b22a474555b9f", "78eb74357b428498a8225b2d753b2fe9a463f89e"], # v0.41.0 10 | ["0bb3b822053c813ab6f695c9194089ccb5186cc3", "78eb74357b428498a8225b2d753b2fe9a463f89e"], # v0.42.0 11 | ["0bb3b822053c813ab6f695c9194089ccb5186cc3", "78eb74357b428498a8225b2d753b2fe9a463f89e"], # v0.42.0 12 | ["0f594732b063a90d44df8c5d402d658f27471dfe", "78eb74357b428498a8225b2d753b2fe9a463f89e"], # v0.43.0 13 | ["0c7a7e2d569eeed9d6025f3eef4ea0690d90845d", "78eb74357b428498a8225b2d753b2fe9a463f89e"], # v0.44.0 14 | ["4520b30d498daca8079365bdb909a8dea38e8d55", "78eb74357b428498a8225b2d753b2fe9a463f89e"], # v0.44.1 15 | ["f044e4c9514ec89c4c1fc8a523ca90b8cb907fb7", "070ca398300bf5b9e1a636ca057882c0312c228d"], # CMonitor* -> PHLMONITOR 16 | ["a425fbebe4cf4238e48a42f724ef2208959d66cf", "a86ed5581498186ed31241c0c246629ef771d1e6"], # v0.45.0 17 | ["500d2a3580388afc8b620b0a3624147faa34f98b", "cb929099477407116031010905ce439db771dd62"], # v0.45.1 18 | ["12f9a0d0b93f691d4d9923716557154d74777b0a", "cb929099477407116031010905ce439db771dd62"], # v0.45.2 19 | ["320144ae7288fe23686935ebb235d9fe0c900862", "0c2c3dc676cee437cece6cca67965bbaba0e45b5"], # CColor -> CHyprColor 20 | ["8bbeee11734d3ba8e2cf15d19cb3c302f8bfdbf2", "b0c1287d571e829adf0b9fe17677c9e7c4277703"], # clang-tidy 21 | ["254fc2bc6000075f660b4b8ed818a6af544d1d64", "9df724651ea92fc0c90ec55e0ef66048c9f76b57"], # v0.46.1 22 | ["0bd541f2fd902dbfa04c3ea2ccf679395e316887", "b2a1da139bbb4e12d67c9c1569a32ce54f5ebc6b"], # v0.46.2 23 | ["983bc067dac2e737bc724721c79d87cd81f27501", "33dc3b3041f3de9a7487839c92a56743e9591f7e"], # animation changes 24 | ["078e13f463d56a4e773aa104bca5567b9e9c8658", "9c5a541e2d8be5e3eff3761d9695615b489023af"], # include fixups 25 | ["e9510115039e7ae9fcf2778e8455e7beed3424ea", "c3fe4a20691cdede553df9b27bc3acebebee8b1c"], # CBox changes 26 | ["04ac46c54357278fc68f0a95d26347ea0db99496", "f383a5cd8fbc9acc1aafbf0fccb161822833b4d6"], # v0.47.0 27 | ["75dff7205f6d2bd437abfb4196f700abee92581a", "f383a5cd8fbc9acc1aafbf0fccb161822833b4d6"], # v0.47.1 28 | ["882f7ad7d2bbfc7440d0ccaef93b1cdd78e8e3ff", "499fd954a95aa2a1fee6cc31c098f6daa5ead3af"], # v0.47.2 29 | ["5ee35f914f921e5696030698e74fb5566a804768", "f7b6c555a064cbf04e185e83dc2db2a843da0fda"], # v0.48.0 30 | ["29e2e59fdbab8ed2cc23a20e3c6043d5decb5cdc", "f7b6c555a064cbf04e185e83dc2db2a843da0fda"], # v0.48.1 31 | ["ddae3036ca6a1729ffe7854a59184116d2622809", "5d2eb7ef9b5ef8d1d3154d51c2c3429aac78ec05"], # missing CConfig include 32 | ["241a4935a244f403fa7108259075b04c81ed258f", "1b5ffdeb06eb4314535dc4d6e960bbc560013ca8"], # class members refactor 33 | ["02d7badd15601e52547e2f3e1c594ceede0a0455", "948d4a8a05a40b4f8ead959972821659a90f0a35"], # class members refactor 34 | ["2ee5118d7a4a59d3ccfaed305bfc05c79cea7637", "18cf673df9807a97ec64a9bb18830f7b8e76e923"], # class members refactor 35 | ["8d6618104e28f6444d917b398244c56ac32ae993", "aca99f87998a698cbdcaa53dc113ffb2be60c946"], # helpers class members refactor 36 | ["46ac115bd19ee3aff5c816033de0b1d55a74e33f", "b4b20b93b49b135b26b332454fa9a70e4f7ac4a5"], # input class members refactor 37 | ["997fefbc1113323ed2bf5d782bdafc0d17532647", "40d9cc4d9529e1e81305bc2681082bdb45118ce9"], # texture class members refactor 38 | ["9958d297641b5c84dcff93f9039d80a5ad37ab00", "51da8d137e042c117fe46a5c019ec38c0de0342a"], # v0.49.0 39 | ## DO NOT EDIT THIS LINE: for auto pin script ## 40 | ] 41 | 42 | [hyprgrass] 43 | description = "Touch screen gestures plugin" 44 | authors = ["horriblename"] 45 | output = "build/src/libhyprgrass.so" 46 | build = [ 47 | "meson setup build", 48 | "ninja -C build", 49 | ] 50 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('hyprgrass', 'cpp', 'c', 2 | version: run_command('cat', join_paths(meson.project_source_root(), 'VERSION'), check: true).stdout().strip(), 3 | default_options: ['buildtype=release'], 4 | ) 5 | 6 | cpp_compiler = meson.get_compiler('cpp') 7 | if cpp_compiler.has_argument('-std=c++23') 8 | add_global_arguments('-std=c++23', language: 'cpp') 9 | elif cpp_compiler.has_argument('-std=c++2b') 10 | add_global_arguments('-std=c++2b', language: 'cpp') 11 | else 12 | error('Could not configure current C++ compiler (' + cpp_compiler.get_id() + ' ' + cpp_compiler.version() + ') with required C++ standard (C++23)') 13 | endif 14 | 15 | hyprland_headers = dependency('hyprland', required: false) 16 | if not hyprland_headers.found() 17 | hyprland_src = run_command('printenv', 'HYPRLAND_HEADERS', check: false).stdout().strip() 18 | if hyprland_src == '' 19 | error('Hyprland not found via pkg-config and environment variable HYPRLAND_HEADERS not set') 20 | endif 21 | 22 | add_global_arguments('-I' + hyprland_src, language: 'cpp') 23 | endif 24 | 25 | if get_option('hyprgrass') 26 | wftouch = dependency('wftouch', required: false) 27 | 28 | if not wftouch.found() and get_option('hyprgrass') 29 | wftouch = subproject('wf-touch').get_variable('wftouch') 30 | endif 31 | endif 32 | 33 | hyprland_deps = [ 34 | dependency('pixman-1'), 35 | dependency('libinput'), 36 | dependency('wayland-server'), 37 | dependency('xkbcommon'), 38 | dependency('libdrm'), 39 | ] 40 | 41 | subdir('examples') 42 | subdir('src') 43 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('tests', type: 'feature', value: 'auto', description: 'Enable unit tests') 2 | option('hyprgrass', type: 'boolean', value: true, description: 'Build main hyprgrass plugin') 3 | option('hyprgrass-pulse', type: 'boolean', value: false, description: 'Build hyprgrass-pulseaudio extension') 4 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | cmake, 4 | meson, 5 | ninja, 6 | hyprland, 7 | hyprlandPlugins, 8 | wf-touch, 9 | doctest, 10 | tag, 11 | commit, 12 | runTests ? false, 13 | ... 14 | }: 15 | hyprlandPlugins.mkHyprlandPlugin hyprland { 16 | pluginName = "hyprgrass"; 17 | version = "${tag}+${commit}"; 18 | src = ./..; 19 | 20 | nativeBuildInputs = [cmake ninja meson] ++ lib.optional runTests doctest; 21 | 22 | buildInputs = [wf-touch doctest]; 23 | 24 | doCheck = true; 25 | 26 | # CMake is just used for finding doctest. 27 | dontUseCmakeConfigure = true; 28 | 29 | meta = with lib; { 30 | homepage = "https://github.com/horriblename/hyprgrass"; 31 | description = "Hyprland plugin for touch gestures"; 32 | license = licenses.bsd3; 33 | platforms = platforms.linux; 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /nix/hyprgrass-pulse.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | meson, 4 | ninja, 5 | pkg-config, 6 | hyprland, 7 | hyprlandPlugins, 8 | libpulseaudio, 9 | tag, 10 | commit, 11 | ... 12 | }: 13 | hyprlandPlugins.mkHyprlandPlugin hyprland { 14 | pluginName = "hyprgrass-pulse"; 15 | version = "${tag}+${commit}"; 16 | src = ./..; 17 | 18 | nativeBuildInputs = [ninja meson pkg-config]; 19 | 20 | buildInputs = [libpulseaudio]; 21 | 22 | mesonFlags = [ 23 | "-Dhyprgrass=false" 24 | "-Dhyprgrass-pulse=true" 25 | "-Dtests=disabled" 26 | ]; 27 | 28 | doCheck = true; 29 | 30 | meta = with lib; { 31 | homepage = "https://github.com/horriblename/hyprgrass"; 32 | description = "Hyprgrass extension to control audio volume"; 33 | license = licenses.bsd3; 34 | platforms = platforms.linux; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /nix/wf-touch.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | stdenv, 4 | fetchFromGitHub, 5 | pkg-config, 6 | meson, 7 | cmake, 8 | ninja, 9 | glm, 10 | doctest, 11 | }: 12 | stdenv.mkDerivation { 13 | pname = "wf-touch"; 14 | version = "git"; 15 | src = fetchFromGitHub { 16 | owner = "WayfireWM"; 17 | repo = "wf-touch"; 18 | rev = "8974eb0f6a65464b63dd03b842795cb441fb6403"; 19 | hash = "sha256-MjsYeKWL16vMKETtKM5xWXszlYUOEk3ghwYI85Lv4SE="; 20 | }; 21 | 22 | nativeBuildInputs = [meson pkg-config cmake ninja]; 23 | buildInputs = [doctest]; 24 | propagatedBuildInputs = [glm]; 25 | 26 | mesonBuildType = "release"; 27 | 28 | patches = [ 29 | ./wf-touch.patch 30 | ]; 31 | 32 | outputs = ["out" "dev"]; 33 | meta = with lib; { 34 | homepage = "https://github.com/WayfireWM/wf-touch"; 35 | license = licenses.mit; 36 | description = "Touchscreen gesture library"; 37 | platforms = platforms.all; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /nix/wf-touch.patch: -------------------------------------------------------------------------------- 1 | diff --git a/meson.build b/meson.build 2 | index 33dbb59..b432d84 100644 3 | --- a/meson.build 4 | +++ b/meson.build 5 | @@ -13,6 +13,9 @@ subdir: 'wayfire/touch') 6 | wftouch_lib = static_library('wftouch', ['src/touch.cpp', 'src/actions.cpp', 'src/math.cpp'], 7 | dependencies: glm, install: true) 8 | 9 | +pkg = import('pkgconfig') 10 | +pkg.generate(wftouch_lib) 11 | + 12 | wftouch = declare_dependency(link_with: wftouch_lib, 13 | include_directories: wf_touch_inc_dirs, dependencies: glm) 14 | 15 | -------------------------------------------------------------------------------- /scripts/ci/latest-hyprland-tag: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ## Usage 4 | ## ----- 5 | ## Prints the tag name of the latest Hyprland release 6 | 7 | gh release list --repo hyprwm/Hyprland --json tagName,isLatest \ 8 | -q 'map(select(.isLatest == true)) | .[0].tagName' 9 | -------------------------------------------------------------------------------- /scripts/ci/pin-latest-hyprland: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | usage() { 6 | echoerr "Usage:" 7 | echoerr "" 8 | echoerr " pin-latest-hyprland [HYPRGRASS_REV]" 9 | echoerr "" 10 | echoerr "Pins the latest hyprland release to HYPRGRASS_REV (default HEAD) in hyprpm.toml." 11 | echoerr "Does nothing if a pin on the hyprland release already exists." 12 | } 13 | 14 | HYPRGRASS_REV=${1:-HEAD} 15 | 16 | echoerr() { 17 | echo $@ >&2 18 | } 19 | 20 | findHyprpmPin() { 21 | local hlCommit="$1" 22 | nix eval --raw --impure --expr ' 23 | with builtins; let 24 | hyprpm = fromTOML (readFile ./hyprpm.toml); 25 | hlCommits = map head hyprpm.repository.commit_pins; 26 | in 27 | concatStringsSep "\n" hlCommits 28 | ' | grep "$hlCommit" > /dev/null 29 | } 30 | 31 | getHyprlandCommitFromRev() { 32 | git ls-remote https://github.com/hyprwm/Hyprland.git "$1" \ 33 | | cut -f 1 34 | } 35 | 36 | SCRIPT_DIR="$(dirname "$0")" 37 | 38 | hlTag="$("$SCRIPT_DIR/latest-hyprland-tag")" 39 | echoerr "found latest Hyprland release: $hlTag" 40 | 41 | hlCommit="$(getHyprlandCommitFromRev "$hlTag")" 42 | echoerr "-> commit: $hlCommit" 43 | 44 | if findHyprpmPin "$hlCommit"; then 45 | echoerr "pin for tag $hlTag already found in hyprpm.toml" 46 | exit 0 47 | fi 48 | 49 | echoerr "building and testing..." 50 | commitPin="$("$SCRIPT_DIR/test-pin.sh" "$hlTag" "$HYPRGRASS_REV")" 51 | 52 | echoerr "pin tests passed: $commitPin" 53 | echoerr 'updating hyprpm.toml' 54 | 55 | sed -i "/## DO NOT EDIT THIS LINE: for auto pin script ##/ i\ $commitPin" \ 56 | hyprpm.toml 57 | 58 | echoerr 'done updating hyprpm.toml' 59 | -------------------------------------------------------------------------------- /scripts/ci/test-pin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eux 4 | 5 | echoerr () { 6 | echo $@ >&2 7 | } 8 | 9 | if [ -z "${1:-}" ]; then 10 | echoerr "Usage:" 11 | echoerr "" 12 | echoerr " test-pin HYPRLAND_REV [HYPRGRASS_REV]" 13 | echoerr "" 14 | echoerr "Runs tests with specified rev and prints the commit SHA of both repo" 15 | echoerr "" 16 | echoerr "HYPRGRASS_REV defaults to 'main'" 17 | exit 1 18 | fi 19 | 20 | hyprlandRev=${1} 21 | hyprgrassRev=${2:-main} 22 | 23 | hyprlandCommit="$(git ls-remote https://github.com/hyprwm/Hyprland.git "${hyprlandRev}" | cut -f 1)" 24 | hyprgrassCommit="$(git rev-parse "${hyprgrassRev}")" 25 | 26 | nix build --no-link "git+file://$(pwd)?rev=${hyprgrassCommit}&shallow=1#hyprgrassWithTests" \ 27 | --override-input hyprland "github:hyprwm/Hyprland/${hyprlandRev}" \ 28 | 29 | echo "[\"${hyprlandCommit}\", \"${hyprgrassCommit}\"], # ${hyprlandRev}" 30 | -------------------------------------------------------------------------------- /scripts/hotreload: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | TMP=/tmp/hyprgrass-testing 6 | helpText=" 7 | usage: hotreload [--unload] FILE 8 | 9 | Hot reload a hyprland plugin 10 | 11 | Plugins loaded by this script will not be 'cached' (not really but don't worry about it :D). 12 | If a new plugin with the same file name is loaded, the old one is first unloaded. 13 | 14 | example: $(basename $0) ./build/libhyprgrass.so 15 | 16 | Subsequent calls will automatically unload the plugin as needed 17 | 18 | Flags: 19 | 20 | --unload Unload only 21 | " 22 | 23 | error () { 24 | echo "$helpText" 25 | exit 1 26 | } 27 | 28 | if [ "$1" = '--unload' ]; then 29 | unloadFlag=1 30 | shift 31 | fi 32 | 33 | if [ -z "$1" ]; then 34 | error 35 | fi 36 | 37 | if ! [ -f "$1" ]; then 38 | echo "'$1' is not a valid file" 39 | echo '' 40 | error 41 | fi 42 | 43 | baseFileName="$(basename "$1")" 44 | fileHash="$(sha256sum "$1" | cut -d ' ' -f 1)" 45 | pluginTempDir="$TMP/$fileHash" 46 | pluginTempFile="$pluginTempDir/$baseFileName" 47 | 48 | for soPath in $TMP/*/$baseFileName; do 49 | if [ -f "$soPath" ]; then 50 | hyprctl plugin unload "$soPath" 51 | rm -r $(dirname "$soPath") 52 | fi 53 | done 54 | 55 | if [ "${unloadFlag}" = 1 ]; then 56 | exit 0 57 | fi 58 | 59 | mkdir -p "$pluginTempDir" 60 | 61 | cp "$1" "$pluginTempDir" 62 | 63 | hyprctl plugin load "$pluginTempFile" 64 | 65 | -------------------------------------------------------------------------------- /src/GestureManager.cpp: -------------------------------------------------------------------------------- 1 | #include "GestureManager.hpp" 2 | #include "HyprLogger.hpp" 3 | #include "globals.hpp" 4 | 5 | #define private public 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #undef private 14 | 15 | #include 16 | 17 | // constexpr double SWIPE_THRESHOLD = 30.; 18 | constexpr int RESIZE_BORDER_GAP_INCREMENT = 10; 19 | 20 | std::string trim(const std::string& str) { 21 | size_t first = str.find_first_not_of(' '); 22 | if (std::string::npos == first) { 23 | return str; 24 | } 25 | size_t last = str.find_last_not_of(' '); 26 | return str.substr(first, (last - first + 1)); 27 | } 28 | 29 | std::vector splitString(const std::string& s, char delimiter, int numSubstrings) { 30 | std::vector substrings; 31 | auto split_view = std::ranges::views::split(s, delimiter); 32 | auto iter = split_view.begin(); 33 | 34 | for (int i = 0; i < numSubstrings - 1 && iter != split_view.end(); ++i, ++iter) { 35 | std::string substring; 36 | for (char c : *iter) { 37 | substring.push_back(c); 38 | } 39 | substrings.push_back(substring); 40 | } 41 | 42 | if (iter != split_view.end()) { 43 | std::string rest; 44 | for (char c : *iter) { 45 | rest.push_back(c); 46 | } 47 | substrings.push_back(rest); 48 | } 49 | 50 | return substrings; 51 | } 52 | 53 | bool emitHookEvent(std::vector* hooks, std::any param) { 54 | SCallbackInfo info; 55 | g_pHookSystem->emit(hooks, info, param); 56 | return info.cancelled; 57 | } 58 | 59 | int handleLongPressTimer(void* data) { 60 | const auto gesture_manager = (GestureManager*)data; 61 | gesture_manager->onLongPressTimeout(gesture_manager->long_press_next_trigger_time); 62 | 63 | return 0; 64 | } 65 | 66 | std::string commaSeparatedCssGaps(CCssGapData data) { 67 | return std::to_string(data.m_top) + "," + std::to_string(data.m_right) + "," + std::to_string(data.m_bottom) + "," + 68 | std::to_string(data.m_left); 69 | } 70 | 71 | GestureManager::GestureManager() : IGestureManager(std::make_unique()) { 72 | static auto const PSENSITIVITY = 73 | (Hyprlang::FLOAT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:sensitivity") 74 | ->getDataStaticPtr(); 75 | static auto const LONG_PRESS_DELAY = 76 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:long_press_delay") 77 | ->getDataStaticPtr(); 78 | static auto const EDGE_MARGIN = 79 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:edge_margin") 80 | ->getDataStaticPtr(); 81 | 82 | this->addEdgeSwipeGesture(*PSENSITIVITY, *LONG_PRESS_DELAY, *EDGE_MARGIN); 83 | this->addLongPress(*PSENSITIVITY, *LONG_PRESS_DELAY); 84 | this->addMultiFingerGesture(*PSENSITIVITY, *LONG_PRESS_DELAY); 85 | this->addMultiFingerTap(*PSENSITIVITY, *LONG_PRESS_DELAY); 86 | 87 | this->long_press_timer = wl_event_loop_add_timer(g_pCompositor->m_wlEventLoop, handleLongPressTimer, this); 88 | } 89 | 90 | GestureManager::~GestureManager() { 91 | wl_event_source_remove(this->long_press_timer); 92 | } 93 | 94 | bool GestureManager::handleCompletedGesture(const CompletedGestureEvent& gev) { 95 | return this->handleGestureBind(gev.to_string(), false); 96 | } 97 | 98 | bool GestureManager::handleDragGesture(const DragGestureEvent& gev) { 99 | static auto const WORKSPACE_SWIPE_FINGERS = 100 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:workspace_swipe_fingers") 101 | ->getDataStaticPtr(); 102 | static auto const WORKSPACE_SWIPE_EDGE = 103 | (Hyprlang::STRING const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:workspace_swipe_edge") 104 | ->getDataStaticPtr(); 105 | static auto const RESIZE_LONG_PRESS = 106 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:resize_on_border_long_press") 107 | ->getDataStaticPtr(); 108 | static auto const EMULATE_TOUCHPAD = 109 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:emulate_touchpad_swipe") 110 | ->getDataStaticPtr(); 111 | 112 | static auto PBORDERSIZE = 113 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "general:border_size")->getDataStaticPtr(); 114 | static auto PBORDERGRABEXTEND = 115 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "general:extend_border_grab_area") 116 | ->getDataStaticPtr(); 117 | static auto PGAPSINDATA = CConfigValue("general:gaps_in"); 118 | 119 | Debug::log(LOG, "[hyprgrass] Drag gesture begin: {}", gev.to_string()); 120 | 121 | auto const workspace_swipe_edge_str = std::string{*WORKSPACE_SWIPE_EDGE}; 122 | 123 | if (g_pSessionLockManager->isSessionLocked()) { 124 | return this->handleGestureBind(gev.to_string(), true); 125 | } 126 | 127 | switch (gev.type) { 128 | case DragGestureType::SWIPE: { 129 | static auto* const PEVENTVEC = g_pHookSystem->getVecForEvent("hyprgrass:swipeBegin"); 130 | 131 | bool handled = emitHookEvent( 132 | PEVENTVEC, std::tuple{ 133 | stringifyDirection(gev.direction), gev.finger_count, 134 | pixelPositionToPercentagePosition(this->m_sGestureState.get_center().origin)}); 135 | 136 | if (handled) { 137 | this->hookHandled = true; 138 | return true; 139 | } 140 | 141 | if (**WORKSPACE_SWIPE_FINGERS != gev.finger_count) { 142 | return false; 143 | } 144 | 145 | if (this->handleWorkspaceSwipe(gev.direction)) 146 | return true; 147 | 148 | if (!**EMULATE_TOUCHPAD) 149 | return false; 150 | 151 | this->workspaceSwipeActive = false; // reset just in case 152 | this->emulatedSwipePoint = this->m_sGestureState.get_center().current; 153 | IPointer::SSwipeBeginEvent swipe = {.fingers = static_cast(m_sGestureState.fingers.size())}; 154 | g_pInputManager->onSwipeBegin(swipe); 155 | return true; 156 | } 157 | 158 | case DragGestureType::EDGE_SWIPE: { 159 | static auto* const PEVENTVEC = g_pHookSystem->getVecForEvent("hyprgrass:edgeBegin"); 160 | 161 | bool handled = emitHookEvent( 162 | PEVENTVEC, 163 | std::pair( 164 | gev.to_string(), pixelPositionToPercentagePosition(this->m_sGestureState.get_center().origin))); 165 | 166 | if (handled) { 167 | this->hookHandled = true; 168 | return true; 169 | } 170 | 171 | if (workspace_swipe_edge_str == "l" && gev.edge_origin == GESTURE_DIRECTION_LEFT) { 172 | return this->handleWorkspaceSwipe(gev.direction); 173 | } 174 | if (workspace_swipe_edge_str == "r" && gev.edge_origin == GESTURE_DIRECTION_RIGHT) { 175 | return this->handleWorkspaceSwipe(gev.direction); 176 | } 177 | if (workspace_swipe_edge_str == "u" && gev.edge_origin == GESTURE_DIRECTION_UP) { 178 | return this->handleWorkspaceSwipe(gev.direction); 179 | } 180 | if (workspace_swipe_edge_str == "d" && gev.edge_origin == GESTURE_DIRECTION_DOWN) { 181 | return this->handleWorkspaceSwipe(gev.direction); 182 | } 183 | 184 | return false; 185 | } 186 | 187 | case DragGestureType::LONG_PRESS: 188 | if (**RESIZE_LONG_PRESS && gev.finger_count == 1) { 189 | const auto BORDER_GRAB_AREA = **PBORDERSIZE + **PBORDERGRABEXTEND; 190 | 191 | // kind of a hack: this is the window detected from previous touch events 192 | const auto w = g_pInputManager->m_foundWindowToFocus.lock(); 193 | const Vector2D touchPos = 194 | pixelPositionToPercentagePosition(this->m_sGestureState.get_center().current) * 195 | this->m_lastTouchedMonitor->m_size; 196 | if (w && !w->isFullscreen()) { 197 | const CBox real = {w->m_realPosition->value().x, w->m_realPosition->value().y, 198 | w->m_realSize->value().x, w->m_realSize->value().y}; 199 | const CBox grab = {real.x - BORDER_GRAB_AREA, real.y - BORDER_GRAB_AREA, 200 | real.width + 2 * BORDER_GRAB_AREA, real.height + 2 * BORDER_GRAB_AREA}; 201 | 202 | bool notInRealWindow = !real.containsPoint(touchPos) || w->isInCurvedCorner(touchPos.x, touchPos.y); 203 | bool onTiledGap = !w->m_isFloating && !w->isFullscreen() && notInRealWindow; 204 | bool inGrabArea = notInRealWindow && grab.containsPoint(touchPos); 205 | 206 | if ((onTiledGap || inGrabArea) && !w->hasPopupAt(touchPos)) { 207 | IPointer::SButtonEvent e = { 208 | .timeMs = 0, // HACK: they don't use this :p 209 | .button = 0, 210 | .state = WL_POINTER_BUTTON_STATE_PRESSED, 211 | }; 212 | g_pKeybindManager->resizeWithBorder(e); 213 | 214 | auto* PGAPSIN = (CCssGapData*)(PGAPSINDATA.ptr())->getData(); 215 | this->resizeOnBorderInfo = { 216 | .active = true, 217 | .old_gaps_in = *PGAPSIN, 218 | }; 219 | 220 | CCssGapData newGapsIn = *PGAPSIN; 221 | newGapsIn.m_top += RESIZE_BORDER_GAP_INCREMENT; 222 | newGapsIn.m_right += RESIZE_BORDER_GAP_INCREMENT; 223 | newGapsIn.m_bottom += RESIZE_BORDER_GAP_INCREMENT; 224 | newGapsIn.m_left += RESIZE_BORDER_GAP_INCREMENT; 225 | g_pConfigManager->parseKeyword("general:gaps_in", commaSeparatedCssGaps(newGapsIn)); 226 | return true; 227 | } 228 | } 229 | } 230 | return this->handleGestureBind(gev.to_string(), true); 231 | } 232 | 233 | return false; 234 | } 235 | 236 | // bind is the name of the gesture event. 237 | // pressed only matters for mouse binds: only start of drag gestures should set it to true 238 | bool GestureManager::handleGestureBind(std::string bind, bool pressed) { 239 | bool found = false; 240 | Debug::log(LOG, "[hyprgrass] Looking for binds matching: {}", bind); 241 | 242 | auto allBinds = std::ranges::views::join(std::array{g_pKeybindManager->m_keybinds, this->internalBinds}); 243 | 244 | for (const auto& k : allBinds) { 245 | if (k->key != bind) 246 | continue; 247 | 248 | const auto DISPATCHER = g_pKeybindManager->m_dispatchers.find(k->mouse ? "mouse" : k->handler); 249 | 250 | // Should never happen, as we check in the ConfigManager, but oh well 251 | if (DISPATCHER == g_pKeybindManager->m_dispatchers.end()) { 252 | Debug::log(ERR, "Invalid handler in a keybind! (handler {} does not exist)", k->handler); 253 | continue; 254 | } 255 | 256 | // call the dispatcher 257 | Debug::log(LOG, "[hyprgrass] calling dispatcher ({})", bind); 258 | 259 | if (k->handler == "pass") 260 | continue; 261 | 262 | if (k->locked != g_pSessionLockManager->isSessionLocked()) 263 | continue; 264 | 265 | if (k->handler == "mouse") { 266 | DISPATCHER->second((pressed ? "1" : "0") + k->arg); 267 | } else { 268 | DISPATCHER->second(k->arg); 269 | } 270 | 271 | if (!k->nonConsuming) { 272 | found = true; 273 | } 274 | } 275 | 276 | return found; 277 | } 278 | 279 | void GestureManager::handleCancelledGesture() {} 280 | 281 | void GestureManager::dragGestureUpdate(const wf::touch::gesture_event_t& ev) { 282 | static auto const EMULATE_TOUCHPAD = 283 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:emulate_touchpad_swipe") 284 | ->getDataStaticPtr(); 285 | 286 | if (!this->getActiveDragGesture().has_value()) { 287 | return; 288 | } 289 | 290 | switch (this->getActiveDragGesture()->type) { 291 | case DragGestureType::SWIPE: 292 | if (this->hookHandled) { 293 | EMIT_HOOK_EVENT("hyprgrass:swipeUpdate", 294 | pixelPositionToPercentagePosition(this->m_sGestureState.get_center().current)) 295 | } else if (this->workspaceSwipeActive) { 296 | this->updateWorkspaceSwipe(); 297 | } else if (**EMULATE_TOUCHPAD) { 298 | const auto currentPoint = this->m_sGestureState.get_center().current; 299 | const auto delta = currentPoint - this->emulatedSwipePoint; 300 | IPointer::SSwipeUpdateEvent swipe = { 301 | .fingers = static_cast(this->getActiveDragGesture()->finger_count), 302 | .delta = Vector2D(delta.x, delta.y)}; 303 | g_pInputManager->onSwipeUpdate(swipe); 304 | this->emulatedSwipePoint = currentPoint; 305 | }; 306 | 307 | case DragGestureType::LONG_PRESS: { 308 | const auto pos = this->m_sGestureState.get_center().current; 309 | g_pCompositor->warpCursorTo(Vector2D(pos.x, pos.y)); 310 | g_pInputManager->simulateMouseMovement(); 311 | return; 312 | } 313 | case DragGestureType::EDGE_SWIPE: 314 | if (this->hookHandled) { 315 | EMIT_HOOK_EVENT("hyprgrass:edgeUpdate", 316 | pixelPositionToPercentagePosition(this->m_sGestureState.get_center().current)) 317 | 318 | return; 319 | } 320 | 321 | this->updateWorkspaceSwipe(); 322 | } 323 | } 324 | 325 | void GestureManager::handleDragGestureEnd(const DragGestureEvent& gev) { 326 | static auto const EMULATE_TOUCHPAD = 327 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:emulate_touchpad_swipe") 328 | ->getDataStaticPtr(); 329 | 330 | if (g_pSessionLockManager->isSessionLocked()) { 331 | this->handleGestureBind(gev.to_string(), false); 332 | return; 333 | } 334 | 335 | Debug::log(LOG, "[hyprgrass] Drag gesture ended: {}", gev.to_string()); 336 | switch (gev.type) { 337 | case DragGestureType::SWIPE: 338 | if (this->hookHandled) { 339 | EMIT_HOOK_EVENT("hyprgrass:swipeEnd", 0) 340 | this->hookHandled = false; 341 | } else if (this->workspaceSwipeActive) { 342 | g_pInputManager->endWorkspaceSwipe(); 343 | this->workspaceSwipeActive = false; 344 | } else if (**EMULATE_TOUCHPAD) { 345 | g_pInputManager->onSwipeEnd(IPointer::SSwipeEndEvent{.cancelled = false}); 346 | } 347 | return; 348 | case DragGestureType::LONG_PRESS: 349 | if (this->resizeOnBorderInfo.active) { 350 | g_pKeybindManager->changeMouseBindMode(eMouseBindMode::MBIND_INVALID); 351 | g_pConfigManager->parseKeyword("general:gaps_in", 352 | commaSeparatedCssGaps(this->resizeOnBorderInfo.old_gaps_in)); 353 | this->resizeOnBorderInfo = {}; 354 | return; 355 | } 356 | 357 | this->handleGestureBind(gev.to_string(), false); 358 | return; 359 | case DragGestureType::EDGE_SWIPE: 360 | if (this->hookHandled) { 361 | EMIT_HOOK_EVENT("hyprgrass:edgeEnd", 0); 362 | this->hookHandled = false; 363 | } else { 364 | g_pInputManager->endWorkspaceSwipe(); 365 | } 366 | } 367 | } 368 | 369 | bool GestureManager::handleWorkspaceSwipe(const GestureDirection direction) { 370 | const bool VERTANIMS = 371 | g_pCompositor->m_lastMonitor->m_activeWorkspace->m_renderOffset->getConfig()->pValues->internalStyle == 372 | "slidevert" || 373 | g_pCompositor->m_lastMonitor->m_activeWorkspace->m_renderOffset->getConfig() 374 | ->pValues->internalStyle.starts_with("slidevert"); 375 | 376 | const auto horizontal = GESTURE_DIRECTION_LEFT | GESTURE_DIRECTION_RIGHT; 377 | const auto vertical = GESTURE_DIRECTION_UP | GESTURE_DIRECTION_DOWN; 378 | const auto workspace_directions = VERTANIMS ? vertical : horizontal; 379 | const auto anti_directions = VERTANIMS ? horizontal : vertical; 380 | 381 | if (direction & workspace_directions && !(direction & anti_directions)) { 382 | this->workspaceSwipeActive = true; 383 | g_pInputManager->beginWorkspaceSwipe(); 384 | return true; 385 | } 386 | 387 | return false; 388 | } 389 | 390 | void GestureManager::updateWorkspaceSwipe() { 391 | const bool VERTANIMS = 392 | g_pInputManager->m_activeSwipe.pWorkspaceBegin->m_renderOffset->getConfig()->pValues->internalStyle == 393 | "slidevert" || 394 | g_pInputManager->m_activeSwipe.pWorkspaceBegin->m_renderOffset->getConfig() 395 | ->pValues->internalStyle.starts_with("slidefadevert"); 396 | 397 | static auto const PSWIPEDIST = 398 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "gestures:workspace_swipe_distance") 399 | ->getDataStaticPtr(); 400 | const auto SWIPEDISTANCE = std::clamp(**PSWIPEDIST, (int64_t)1LL, (int64_t)UINT32_MAX); 401 | 402 | const auto monArea = this->getMonitorArea(); 403 | const auto delta_percent = this->m_sGestureState.get_center().delta() / wf::touch::point_t(monArea.w, monArea.h); 404 | 405 | const auto swipe_delta = Vector2D(delta_percent.x * SWIPEDISTANCE, delta_percent.y * SWIPEDISTANCE); 406 | 407 | g_pInputManager->updateWorkspaceSwipe(VERTANIMS ? -swipe_delta.y : -swipe_delta.x); 408 | return; 409 | } 410 | 411 | void GestureManager::updateLongPressTimer(uint32_t current_time, uint32_t delay) { 412 | this->long_press_next_trigger_time = current_time + delay + 1; 413 | wl_event_source_timer_update(this->long_press_timer, delay); 414 | } 415 | 416 | void GestureManager::stopLongPressTimer() { 417 | wl_event_source_timer_update(this->long_press_timer, 0); 418 | } 419 | 420 | void GestureManager::sendCancelEventsToWindows() { 421 | static auto const SEND_CANCEL = 422 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:experimental:send_cancel") 423 | ->getDataStaticPtr(); 424 | 425 | if (!**SEND_CANCEL) { 426 | return; 427 | } 428 | 429 | for (const auto& touch : this->touchedResources.all()) { 430 | const auto t = touch.lock(); 431 | if (t.get()) { 432 | t->sendCancel(); 433 | } 434 | } 435 | } 436 | 437 | // @return whether or not to inhibit further actions 438 | bool GestureManager::onTouchDown(ITouch::SDownEvent ev) { 439 | static auto const SEND_CANCEL = 440 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:experimental:send_cancel") 441 | ->getDataStaticPtr(); 442 | 443 | this->m_lastTouchedMonitor = 444 | g_pCompositor->getMonitorFromName(!ev.device->m_boundOutput.empty() ? ev.device->m_boundOutput : ""); 445 | 446 | this->m_lastTouchedMonitor = 447 | this->m_lastTouchedMonitor ? this->m_lastTouchedMonitor : g_pCompositor->m_lastMonitor.lock(); 448 | 449 | const auto& monitorPos = this->m_lastTouchedMonitor->m_position; 450 | const auto& monitorSize = this->m_lastTouchedMonitor->m_size; 451 | this->m_monitorArea = {monitorPos.x, monitorPos.y, monitorSize.x, monitorSize.y}; 452 | 453 | g_pCompositor->warpCursorTo({ 454 | monitorPos.x + ev.pos.x * monitorSize.x, 455 | monitorPos.y + ev.pos.y * monitorSize.y, 456 | }); 457 | 458 | g_pInputManager->refocus(); 459 | 460 | if (this->m_sGestureState.fingers.size() == 0) { 461 | this->touchedResources.clear(); 462 | this->hookHandled = false; 463 | } 464 | 465 | if (!eventForwardingInhibited() && **SEND_CANCEL && g_pInputManager->m_touchData.touchFocusSurface) { 466 | // remember which surfaces were touched, to later send cancel events 467 | const auto surface = g_pInputManager->m_touchData.touchFocusSurface; 468 | 469 | wl_client* client = surface.get()->client(); 470 | if (client) { 471 | SP seat = g_pSeatManager->seatResourceForClient(client); 472 | 473 | if (seat) { 474 | auto touches = seat.get()->m_touches; 475 | for (const auto& touch : touches) { 476 | this->touchedResources.insert(touch); 477 | } 478 | } 479 | } 480 | } 481 | 482 | // NOTE @wlr_touch_down_event.x and y uses a number between 0 and 1 to 483 | // represent "how many percent of screen" whereas 484 | // @wf::touch::gesture_event_t uses PIXELS as unit 485 | auto pos = wlrTouchEventPositionAsPixels(ev.pos.x, ev.pos.y); 486 | 487 | const wf::touch::gesture_event_t gesture_event = { 488 | .type = wf::touch::EVENT_TYPE_TOUCH_DOWN, 489 | .time = ev.timeMs, 490 | .finger = ev.touchID, 491 | .pos = pos, 492 | }; 493 | 494 | return IGestureManager::onTouchDown(gesture_event); 495 | } 496 | 497 | bool GestureManager::onTouchUp(ITouch::SUpEvent ev) { 498 | static auto const SEND_CANCEL = 499 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:experimental:send_cancel") 500 | ->getDataStaticPtr(); 501 | 502 | wf::touch::point_t lift_off_pos; 503 | try { 504 | lift_off_pos = this->m_sGestureState.fingers.at(ev.touchID).current; 505 | } catch (const std::out_of_range&) { 506 | return false; 507 | } 508 | 509 | const wf::touch::gesture_event_t gesture_event = { 510 | .type = wf::touch::EVENT_TYPE_TOUCH_UP, 511 | .time = ev.timeMs, 512 | .finger = ev.touchID, 513 | .pos = {lift_off_pos.x, lift_off_pos.y}, 514 | }; 515 | 516 | const auto BLOCK = IGestureManager::onTouchUp(gesture_event); 517 | if (**SEND_CANCEL) { 518 | const auto surface = g_pInputManager->m_touchData.touchFocusSurface; 519 | 520 | if (!surface.valid()) { 521 | return true; 522 | } 523 | 524 | wl_client* client = surface.get()->client(); 525 | if (!client) { 526 | return true; 527 | } 528 | 529 | SP seat = g_pSeatManager->seatResourceForClient(client); 530 | if (!seat.get()) { 531 | return true; 532 | } 533 | 534 | auto touches = seat.get()->m_touches; 535 | for (const auto& touch : touches) { 536 | this->touchedResources.remove(touch); 537 | } 538 | 539 | return BLOCK; 540 | } else { 541 | // send_cancel is turned off; we need to rely on touchup events 542 | return false; 543 | } 544 | } 545 | 546 | bool GestureManager::onTouchMove(ITouch::SMotionEvent ev) { 547 | auto pos = wlrTouchEventPositionAsPixels(ev.pos.x, ev.pos.y); 548 | 549 | const wf::touch::gesture_event_t gesture_event = { 550 | .type = wf::touch::EVENT_TYPE_MOTION, 551 | .time = ev.timeMs, 552 | .finger = ev.touchID, 553 | .pos = pos, 554 | }; 555 | 556 | return IGestureManager::onTouchMove(gesture_event); 557 | } 558 | 559 | SMonitorArea GestureManager::getMonitorArea() const { 560 | return this->m_monitorArea; 561 | } 562 | 563 | void GestureManager::onLongPressTimeout(uint32_t time_msec) { 564 | if (this->m_sGestureState.fingers.empty()) { 565 | return; 566 | } 567 | 568 | const auto finger = this->m_sGestureState.fingers.begin(); 569 | 570 | const wf::touch::gesture_event_t touch_event = { 571 | .type = wf::touch::EVENT_TYPE_MOTION, 572 | .time = time_msec, 573 | .finger = finger->first, 574 | .pos = finger->second.current, 575 | }; 576 | 577 | IGestureManager::onTouchMove(touch_event); 578 | } 579 | 580 | wf::touch::point_t GestureManager::wlrTouchEventPositionAsPixels(double x, double y) const { 581 | auto area = this->getMonitorArea(); 582 | return wf::touch::point_t{x * area.w + area.x, y * area.h + area.y}; 583 | } 584 | 585 | Vector2D GestureManager::pixelPositionToPercentagePosition(wf::touch::point_t point) const { 586 | auto monitorArea = this->getMonitorArea(); 587 | return Vector2D((point.x - monitorArea.x) / monitorArea.w, (point.y - monitorArea.y) / monitorArea.h); 588 | } 589 | 590 | void GestureManager::touchBindDispatcher(std::string args) { 591 | auto argsSplit = splitString(args, ',', 4); 592 | if (argsSplit.size() < 4) { 593 | Debug::log(ERR, "touchBind called with not enough args: {}", args); 594 | return; 595 | } 596 | const auto _modifier = trim(argsSplit[0]); 597 | const auto key = trim(argsSplit[1]); 598 | const auto dispatcher = trim(argsSplit[2]); 599 | const auto dispatcherArgs = trim(argsSplit[3]); 600 | 601 | this->internalBinds.emplace_back(makeShared(SKeybind{ 602 | .key = key, 603 | .handler = dispatcher, 604 | .arg = dispatcherArgs, 605 | })); 606 | } 607 | -------------------------------------------------------------------------------- /src/GestureManager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "./gestures/Gestures.hpp" 3 | #include "VecSet.hpp" 4 | 5 | #define private public 6 | #include 7 | #include 8 | #include 9 | #undef private 10 | 11 | class GestureManager : public IGestureManager { 12 | public: 13 | uint32_t long_press_next_trigger_time; 14 | std::vector> internalBinds; 15 | 16 | GestureManager(); 17 | ~GestureManager(); 18 | // @return whether this touch event should be blocked from forwarding to the 19 | // client window/surface 20 | bool onTouchDown(ITouch::SDownEvent e); 21 | 22 | // @return whether this touch event should be blocked from forwarding to the 23 | // client window/surface 24 | bool onTouchUp(ITouch::SUpEvent e); 25 | 26 | // @return whether this touch event should be blocked from forwarding to the 27 | // client window/surface 28 | bool onTouchMove(ITouch::SMotionEvent e); 29 | 30 | void onLongPressTimeout(uint32_t time_msec); 31 | 32 | // workaround 33 | void touchBindDispatcher(std::string args); 34 | 35 | protected: 36 | SMonitorArea getMonitorArea() const override; 37 | bool handleCompletedGesture(const CompletedGestureEvent& gev) override; 38 | void handleCancelledGesture() override; 39 | 40 | private: 41 | VecSet> touchedResources; 42 | PHLMONITOR m_lastTouchedMonitor; 43 | SMonitorArea m_monitorArea; 44 | wl_event_source* long_press_timer; 45 | struct { 46 | bool active = false; 47 | CCssGapData old_gaps_in; 48 | } resizeOnBorderInfo; 49 | bool workspaceSwipeActive = false; 50 | bool hookHandled = false; 51 | wf::touch::point_t emulatedSwipePoint; 52 | 53 | bool handleGestureBind(std::string bind, bool pressed); 54 | 55 | // converts wlr touch event positions (number between 0.0 to 1.0) to pixel position, 56 | // takes into consideration monitor size and offset 57 | wf::touch::point_t wlrTouchEventPositionAsPixels(double x, double y) const; 58 | // reverse of wlrTouchEventPositionAsPixels 59 | Vector2D pixelPositionToPercentagePosition(wf::touch::point_t) const; 60 | bool handleWorkspaceSwipe(const GestureDirection direction); 61 | void updateWorkspaceSwipe(); 62 | 63 | bool handleDragGesture(const DragGestureEvent& gev) override; 64 | void dragGestureUpdate(const wf::touch::gesture_event_t&) override; 65 | void handleDragGestureEnd(const DragGestureEvent& gev) override; 66 | 67 | void updateLongPressTimer(uint32_t current_time, uint32_t delay) override; 68 | void stopLongPressTimer() override; 69 | 70 | void sendCancelEventsToWindows() override; 71 | }; 72 | 73 | inline std::unique_ptr g_pGestureManager; 74 | -------------------------------------------------------------------------------- /src/HyprLogger.hpp: -------------------------------------------------------------------------------- 1 | #include "gestures/Logger.hpp" 2 | #include 3 | 4 | class HyprLogger : public Logger { 5 | public: 6 | void debug(std::string s) { 7 | Debug::log(INFO, "[hyprgrass] {}", s); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/TouchVisualizer.cpp: -------------------------------------------------------------------------------- 1 | #include "TouchVisualizer.hpp" 2 | #include 3 | #include 4 | 5 | CBox boxAroundCenter(Vector2D center, double radius) { 6 | return CBox(center.x - radius, center.y - radius, 2 * radius, 2 * radius); 7 | } 8 | 9 | Visualizer::Visualizer() { 10 | const int R = TOUCH_POINT_RADIUS; 11 | 12 | this->cairoSurface = 13 | cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 2 * TOUCH_POINT_RADIUS, 2 * TOUCH_POINT_RADIUS); 14 | auto cairo = cairo_create(cairoSurface); 15 | 16 | cairo_arc(cairo, R, R, R, 0, 2 * PI); 17 | cairo_set_source_rgba(cairo, 0.8, 0.8, 0.1, 0.6); 18 | cairo_fill(cairo); 19 | 20 | cairo_destroy(cairo); 21 | 22 | const unsigned char* data = cairo_image_surface_get_data(this->cairoSurface); 23 | 24 | this->texture->allocate(); 25 | glBindTexture(GL_TEXTURE_2D, this->texture->m_texID); 26 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); 27 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 28 | 29 | glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 2 * TOUCH_POINT_RADIUS, 2 * TOUCH_POINT_RADIUS, 0, GL_RGBA, 30 | GL_UNSIGNED_BYTE, data); 31 | } 32 | 33 | Visualizer::~Visualizer() { 34 | if (this->cairoSurface) 35 | cairo_surface_destroy(this->cairoSurface); 36 | } 37 | 38 | void Visualizer::onPreRender() {} 39 | 40 | void Visualizer::onRender() { 41 | if (this->finger_positions.size() < 1) { 42 | return; 43 | } 44 | 45 | const auto monitor = g_pCompositor->m_lastMonitor.lock(); 46 | 47 | // HACK: should not damage monitor, however, I don't understand jackshit 48 | // about damage so here we are. 49 | // If you know how to do damage properly I BEG OF YOU PLEASE ABSOLVE ME 50 | // OF MY SINS 51 | if (this->finger_positions.size()) { 52 | g_pHyprRenderer->damageMonitor(monitor); 53 | } 54 | 55 | for (auto& finger : this->finger_positions) { 56 | CBox dmg = boxAroundCenter(finger.second.curr, TOUCH_POINT_RADIUS); 57 | g_pHyprOpenGL->renderTexture(this->texture, dmg, 1.f, 0, true); 58 | } 59 | } 60 | 61 | void Visualizer::onTouchDown(ITouch::SDownEvent ev) { 62 | auto mon = g_pCompositor->m_lastMonitor.lock(); 63 | this->finger_positions.emplace(ev.touchID, FingerPos{ev.pos * mon->m_pixelSize + mon->m_position, std::nullopt}); 64 | g_pCompositor->scheduleFrameForMonitor(mon); 65 | } 66 | 67 | void Visualizer::onTouchUp(ITouch::SUpEvent ev) { 68 | this->damageFinger(ev.touchID); 69 | this->finger_positions.erase(ev.touchID); 70 | g_pCompositor->scheduleFrameForMonitor(g_pCompositor->m_lastMonitor.lock()); 71 | } 72 | 73 | void Visualizer::onTouchMotion(ITouch::SMotionEvent ev) { 74 | auto mon = g_pCompositor->m_lastMonitor.lock(); 75 | this->finger_positions[ev.touchID] = {ev.pos * mon->m_pixelSize + mon->m_position, std::nullopt}; 76 | g_pCompositor->scheduleFrameForMonitor(mon); 77 | } 78 | 79 | void Visualizer::damageFinger(int32_t id) { 80 | auto finger = this->finger_positions.at(id); 81 | 82 | CBox dm = boxAroundCenter(finger.curr, TOUCH_POINT_RADIUS); 83 | g_pHyprRenderer->damageBox(dm); 84 | 85 | if (finger.last_rendered.has_value()) { 86 | dm = boxAroundCenter(finger.last_rendered.value(), TOUCH_POINT_RADIUS); 87 | g_pHyprRenderer->damageBox(dm); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/TouchVisualizer.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | struct FingerPos { 6 | Vector2D curr; 7 | std::optional last_rendered; 8 | }; 9 | 10 | class Visualizer { 11 | public: 12 | Visualizer(); 13 | ~Visualizer(); 14 | void onPreRender(); 15 | void onRender(); 16 | void damageFinger(int32_t id); 17 | 18 | void onTouchDown(ITouch::SDownEvent); 19 | void onTouchUp(ITouch::SUpEvent); 20 | void onTouchMotion(ITouch::SMotionEvent); 21 | 22 | private: 23 | SP texture = makeShared(); 24 | cairo_surface_t* cairoSurface; 25 | bool tempDamaged = false; 26 | const int TOUCH_POINT_RADIUS = 30; 27 | std::unordered_map finger_positions; 28 | }; 29 | -------------------------------------------------------------------------------- /src/VecSet.cpp: -------------------------------------------------------------------------------- 1 | #include "VecSet.hpp" 2 | 3 | template bool VecSet::has(const T x) { 4 | for (const auto& i : this->set) { 5 | if (i == x) { 6 | return true; 7 | } 8 | } 9 | 10 | return false; 11 | } 12 | 13 | template bool VecSet::insert(const T x) { 14 | if (this->has(x)) { 15 | return true; 16 | } 17 | 18 | this->set.push_back(x); 19 | return false; 20 | } 21 | 22 | template bool VecSet::remove(const T x) { 23 | for (size_t i = 0; i < this->set.size(); i++) { 24 | if (this->set[i] == x) { 25 | if (i != this->set.size() - 1) { 26 | this->set[i] = this->set.back(); 27 | } 28 | 29 | this->set.pop_back(); 30 | return true; 31 | } 32 | } 33 | 34 | return false; 35 | } 36 | 37 | template void VecSet::clear() { 38 | this->set.clear(); 39 | } 40 | 41 | template const std::vector& VecSet::all() const { 42 | return this->set; 43 | } 44 | -------------------------------------------------------------------------------- /src/VecSet.hpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | // Probably not compatible with move semantics, I really don't know 5 | template class VecSet { 6 | public: 7 | bool has(const T x); 8 | 9 | // returns whether or not it already exists prior to insert 10 | bool insert(const T x); 11 | 12 | // returns whether or not x was found in the set 13 | bool remove(const T x); 14 | 15 | void clear(); 16 | 17 | const std::vector& all() const; 18 | 19 | private: 20 | std::vector set; 21 | }; 22 | 23 | // For some reason if I put this anywhere else the symbols don't get compiled in. 24 | // C++ is a beauty 25 | template class VecSet>; 26 | -------------------------------------------------------------------------------- /src/gestures/Actions.cpp: -------------------------------------------------------------------------------- 1 | #include "Actions.hpp" 2 | #include 3 | 4 | wf::touch::action_status_t CMultiAction::update_state(const wf::touch::gesture_state_t& state, 5 | const wf::touch::gesture_event_t& event) { 6 | if (event.time - this->start_time > *this->timeout) { 7 | return wf::touch::ACTION_STATUS_CANCELLED; 8 | } 9 | 10 | if (event.type == wf::touch::EVENT_TYPE_TOUCH_UP) { 11 | return wf::touch::ACTION_STATUS_CANCELLED; 12 | } 13 | 14 | if (event.type == wf::touch::EVENT_TYPE_TOUCH_DOWN) { 15 | // cancel if previous fingers moved too much 16 | this->finger_count = state.fingers.size(); 17 | for (auto& finger : state.fingers) { 18 | // TODO multiply tolerance by sensitivity? 19 | if (glm::length(finger.second.delta()) > GESTURE_INITIAL_TOLERANCE) { 20 | return wf::touch::ACTION_STATUS_CANCELLED; 21 | } 22 | } 23 | 24 | return wf::touch::ACTION_STATUS_RUNNING; 25 | } 26 | 27 | if ((glm::length(state.get_center().delta()) >= MIN_SWIPE_DISTANCE) && (this->target_direction == 0)) { 28 | this->target_direction = state.get_center().get_direction(); 29 | } 30 | 31 | if (this->target_direction == 0) { 32 | return wf::touch::ACTION_STATUS_RUNNING; 33 | } 34 | 35 | for (auto& finger : state.fingers) { 36 | if (finger.second.get_incorrect_drag_distance(this->target_direction) > this->get_move_tolerance()) { 37 | return wf::touch::ACTION_STATUS_CANCELLED; 38 | } 39 | } 40 | 41 | if (state.get_center().get_drag_distance(target_direction) >= base_threshold / *sensitivity) { 42 | return wf::touch::ACTION_STATUS_COMPLETED; 43 | } 44 | return wf::touch::ACTION_STATUS_RUNNING; 45 | } 46 | 47 | wf::touch::action_status_t MultiFingerDownAction::update_state(const wf::touch::gesture_state_t& state, 48 | const wf::touch::gesture_event_t& event) { 49 | if (event.time - this->start_time > this->get_duration()) { 50 | return wf::touch::ACTION_STATUS_CANCELLED; 51 | } 52 | 53 | if (event.type == wf::touch::EVENT_TYPE_TOUCH_UP) { 54 | return wf::touch::ACTION_STATUS_CANCELLED; 55 | } 56 | 57 | if (event.type == wf::touch::EVENT_TYPE_TOUCH_DOWN && state.fingers.size() >= SEND_CANCEL_EVENT_FINGER_COUNT) { 58 | return wf::touch::ACTION_STATUS_COMPLETED; 59 | } 60 | 61 | return wf::touch::ACTION_STATUS_RUNNING; 62 | } 63 | 64 | wf::touch::action_status_t MultiFingerTap::update_state(const wf::touch::gesture_state_t& state, 65 | const wf::touch::gesture_event_t& event) { 66 | if (event.time - this->start_time > *this->timeout) { 67 | return wf::touch::ACTION_STATUS_CANCELLED; 68 | } 69 | 70 | if (event.type == wf::touch::EVENT_TYPE_TOUCH_UP) { 71 | return wf::touch::ACTION_STATUS_COMPLETED; 72 | } 73 | 74 | if (event.type == wf::touch::EVENT_TYPE_MOTION) { 75 | for (const auto& finger : state.fingers) { 76 | const auto delta = finger.second.delta(); 77 | if (delta.x * delta.x + delta.y + delta.y > this->base_threshold / *this->sensitivity) { 78 | return wf::touch::ACTION_STATUS_CANCELLED; 79 | } 80 | } 81 | } 82 | 83 | return wf::touch::ACTION_STATUS_RUNNING; 84 | } 85 | 86 | wf::touch::action_status_t LongPress::update_state(const wf::touch::gesture_state_t& state, 87 | const wf::touch::gesture_event_t& event) { 88 | if (event.time - this->start_time > *this->delay) { 89 | return wf::touch::ACTION_STATUS_COMPLETED; 90 | } 91 | 92 | switch (event.type) { 93 | case wf::touch::EVENT_TYPE_MOTION: 94 | for (const auto& finger : state.fingers) { 95 | const auto delta = finger.second.delta(); 96 | if (delta.x * delta.x + delta.y + delta.y > this->base_threshold / *this->sensitivity) { 97 | return wf::touch::ACTION_STATUS_CANCELLED; 98 | } 99 | } 100 | break; 101 | 102 | case wf::touch::EVENT_TYPE_TOUCH_DOWN: 103 | // TODO: also reset wl_timer here 104 | gesture_action_t::reset(event.time); 105 | this->update_external_timer_callback(event.time, *this->delay); 106 | break; 107 | 108 | case wf::touch::EVENT_TYPE_TOUCH_UP: 109 | return wf::touch::ACTION_STATUS_CANCELLED; 110 | } 111 | 112 | return wf::touch::ACTION_STATUS_RUNNING; 113 | } 114 | 115 | wf::touch::action_status_t LiftoffAction::update_state(const wf::touch::gesture_state_t& state, 116 | const wf::touch::gesture_event_t& event) { 117 | if (event.time - this->start_time > this->get_duration()) { 118 | return wf::touch::ACTION_STATUS_CANCELLED; 119 | } 120 | 121 | if (event.type == wf::touch::EVENT_TYPE_TOUCH_UP) { 122 | return wf::touch::ACTION_STATUS_COMPLETED; 123 | } 124 | 125 | if (event.type == wf::touch::EVENT_TYPE_TOUCH_DOWN) { 126 | return wf::touch::ACTION_STATUS_CANCELLED; 127 | } 128 | 129 | return wf::touch::ACTION_STATUS_RUNNING; 130 | } 131 | 132 | wf::touch::action_status_t TouchUpOrDownAction::update_state(const wf::touch::gesture_state_t& state, 133 | const wf::touch::gesture_event_t& event) { 134 | if (event.time - this->start_time > this->get_duration()) { 135 | return wf::touch::ACTION_STATUS_CANCELLED; 136 | } 137 | 138 | if (event.type == wf::touch::EVENT_TYPE_TOUCH_UP || event.type == wf::touch::EVENT_TYPE_TOUCH_DOWN) { 139 | return wf::touch::ACTION_STATUS_COMPLETED; 140 | } 141 | 142 | return wf::touch::ACTION_STATUS_RUNNING; 143 | } 144 | 145 | wf::touch::action_status_t LiftAll::update_state(const wf::touch::gesture_state_t& state, 146 | const wf::touch::gesture_event_t& event) { 147 | if (event.time - this->start_time > this->get_duration()) { 148 | return wf::touch::ACTION_STATUS_CANCELLED; 149 | } 150 | 151 | if (event.type == wf::touch::EVENT_TYPE_TOUCH_UP && state.fingers.size() == 0) { 152 | return wf::touch::ACTION_STATUS_COMPLETED; 153 | } 154 | 155 | return wf::touch::ACTION_STATUS_RUNNING; 156 | } 157 | wf::touch::action_status_t OnCompleteAction::update_state(const wf::touch::gesture_state_t& state, 158 | const wf::touch::gesture_event_t& event) { 159 | auto status = this->action->update_state(state, event); 160 | 161 | if (status == wf::touch::ACTION_STATUS_COMPLETED) { 162 | this->callback(); 163 | } 164 | 165 | return status; 166 | } 167 | -------------------------------------------------------------------------------- /src/gestures/Actions.hpp: -------------------------------------------------------------------------------- 1 | #include "Shared.hpp" 2 | #include 3 | #include 4 | #include 5 | 6 | using UpdateExternalTimerCallback = std::function; 7 | 8 | // swipe and with multiple fingers and directions 9 | class CMultiAction : public wf::touch::gesture_action_t { 10 | private: 11 | double base_threshold; 12 | const float* sensitivity; 13 | const int64_t* timeout; 14 | 15 | public: 16 | // threshold = base_threshold / sensitivity 17 | // if the threshold needs to be adjusted dynamically, the sensitivity 18 | // pointer is used 19 | CMultiAction(double base_threshold, const float* sensitivity, const int64_t* timeout) 20 | : base_threshold(base_threshold), sensitivity(sensitivity), timeout(timeout){}; 21 | 22 | GestureDirection target_direction = 0; 23 | int finger_count = 0; 24 | 25 | // The action is completed if any number of fingers is moved enough. 26 | // 27 | // This action should be followed by another that completes upon lifting a 28 | // finger to achieve a gesture that completes after a multi-finger swipe is 29 | // done and lifted. 30 | wf::touch::action_status_t update_state(const wf::touch::gesture_state_t& state, 31 | const wf::touch::gesture_event_t& event) override; 32 | 33 | void reset(uint32_t time) override { 34 | gesture_action_t::reset(time); 35 | target_direction = 0; 36 | }; 37 | }; 38 | 39 | class MultiFingerTap : public wf::touch::gesture_action_t { 40 | private: 41 | double base_threshold; 42 | const float* sensitivity; 43 | const int64_t* timeout; 44 | 45 | public: 46 | MultiFingerTap(double base_threshold, const float* sensitivity, const int64_t* timeout) 47 | : base_threshold(base_threshold), sensitivity(sensitivity), timeout(timeout){}; 48 | 49 | wf::touch::action_status_t update_state(const wf::touch::gesture_state_t& state, 50 | const wf::touch::gesture_event_t& event) override; 51 | }; 52 | 53 | class LongPress : public wf::touch::gesture_action_t { 54 | private: 55 | double base_threshold; 56 | const float* sensitivity; 57 | const int64_t* delay; 58 | UpdateExternalTimerCallback update_external_timer_callback; 59 | 60 | public: 61 | // TODO: I hope one day I can figure out how not to pass a function for the update timer callback 62 | LongPress(double base_threshold, const float* sensitivity, const int64_t* delay, 63 | UpdateExternalTimerCallback update_external_timer) 64 | : base_threshold(base_threshold), sensitivity(sensitivity), delay(delay), 65 | update_external_timer_callback(update_external_timer){}; 66 | 67 | wf::touch::action_status_t update_state(const wf::touch::gesture_state_t& state, 68 | const wf::touch::gesture_event_t& event) override; 69 | }; 70 | 71 | // Completes upon receiving enough touch down events within a short duration 72 | class MultiFingerDownAction : public wf::touch::gesture_action_t { 73 | // upon completion, calls the given callback. 74 | // 75 | // Intended to be used to send cancel events to surfaces when enough fingers 76 | // touch down in quick succession. 77 | public: 78 | MultiFingerDownAction() {} 79 | 80 | wf::touch::action_status_t update_state(const wf::touch::gesture_state_t& state, 81 | const wf::touch::gesture_event_t& event) override; 82 | }; 83 | 84 | // Completes upon receiving a touch up event and cancels upon receiving a touch 85 | // down event. 86 | class LiftoffAction : public wf::touch::gesture_action_t { 87 | wf::touch::action_status_t update_state(const wf::touch::gesture_state_t& state, 88 | const wf::touch::gesture_event_t& event) override; 89 | }; 90 | 91 | // Completes upon receiving a touch up or touch down event 92 | class TouchUpOrDownAction : public wf::touch::gesture_action_t { 93 | wf::touch::action_status_t update_state(const wf::touch::gesture_state_t& state, 94 | const wf::touch::gesture_event_t& event) override; 95 | }; 96 | 97 | // Completes upon all touch points lifted. 98 | class LiftAll : public wf::touch::gesture_action_t { 99 | wf::touch::action_status_t update_state(const wf::touch::gesture_state_t& state, 100 | const wf::touch::gesture_event_t& event) override; 101 | }; 102 | 103 | // This action is used to call a function right after another action is completed 104 | class OnCompleteAction : public wf::touch::gesture_action_t { 105 | private: 106 | std::unique_ptr action; 107 | const std::function callback; 108 | 109 | public: 110 | OnCompleteAction(std::unique_ptr action, std::function callback) 111 | : callback(callback) { 112 | this->action = std::move(action); 113 | } 114 | 115 | wf::touch::action_status_t update_state(const wf::touch::gesture_state_t& state, 116 | const wf::touch::gesture_event_t& event) override; 117 | 118 | void reset(uint32_t time) override { 119 | this->action->reset(time); 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /src/gestures/CompletedGesture.cpp: -------------------------------------------------------------------------------- 1 | #include "CompletedGesture.hpp" 2 | 3 | std::string CompletedGestureEvent::to_string() const { 4 | switch (type) { 5 | case CompletedGestureType::EDGE_SWIPE: 6 | return "edge:" + stringifyDirection(this->edge_origin) + ":" + stringifyDirection(this->direction); 7 | case CompletedGestureType::SWIPE: 8 | return "swipe:" + std::to_string(finger_count) + ":" + stringifyDirection(this->direction); 9 | break; 10 | case CompletedGestureType::TAP: 11 | return "tap:" + std::to_string(finger_count); 12 | case CompletedGestureType::LONG_PRESS: 13 | return "longpress:" + std::to_string(finger_count); 14 | } 15 | 16 | return ""; 17 | } 18 | -------------------------------------------------------------------------------- /src/gestures/CompletedGesture.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Shared.hpp" 3 | #include 4 | 5 | enum class CompletedGestureType { 6 | // Invalid Gesture 7 | SWIPE, 8 | EDGE_SWIPE, 9 | TAP, 10 | LONG_PRESS, 11 | // PINCH, 12 | }; 13 | 14 | struct CompletedGestureEvent { 15 | CompletedGestureType type; 16 | GestureDirection direction; 17 | int finger_count; 18 | 19 | // TODO turn this whole struct into a sum type? 20 | // edge swipe specific 21 | GestureDirection edge_origin; 22 | 23 | std::string to_string() const; 24 | }; 25 | -------------------------------------------------------------------------------- /src/gestures/DragGesture.cpp: -------------------------------------------------------------------------------- 1 | #include "DragGesture.hpp" 2 | 3 | std::string DragGestureEvent::to_string() const { 4 | switch (type) { 5 | case DragGestureType::LONG_PRESS: 6 | return "longpress:" + std::to_string(finger_count); 7 | case DragGestureType::SWIPE: 8 | return "swipe:" + std::to_string(finger_count) + ":" + stringifyDirection(this->direction); 9 | case DragGestureType::EDGE_SWIPE: 10 | return "edge:" + stringifyDirection(this->edge_origin) + ":" + stringifyDirection(this->direction); 11 | } 12 | 13 | return ""; 14 | } 15 | -------------------------------------------------------------------------------- /src/gestures/DragGesture.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "Shared.hpp" 3 | 4 | enum class DragGestureType { 5 | SWIPE, 6 | LONG_PRESS, 7 | EDGE_SWIPE, 8 | }; 9 | 10 | struct DragGestureEvent { 11 | DragGestureType type; 12 | GestureDirection direction; 13 | int finger_count; 14 | 15 | GestureDirection edge_origin; 16 | 17 | std::string to_string() const; 18 | }; 19 | -------------------------------------------------------------------------------- /src/gestures/Gestures.cpp: -------------------------------------------------------------------------------- 1 | #include "Gestures.hpp" 2 | #include "Actions.hpp" 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | void IGestureManager::updateGestures(const wf::touch::gesture_event_t& ev) { 10 | if (m_sGestureState.fingers.size() == 1 && ev.type == wf::touch::EVENT_TYPE_TOUCH_DOWN) { 11 | this->inhibitTouchEvents = false; 12 | this->activeDragGesture = std::nullopt; 13 | } 14 | bool should_reset = m_sGestureState.fingers.size() == 1 && ev.type == wf::touch::EVENT_TYPE_TOUCH_DOWN; 15 | this->gestureTriggered = false; 16 | for (const auto& gesture : m_vGestures) { 17 | if (should_reset) { 18 | gesture->reset(ev.time); 19 | } 20 | 21 | if (!this->gestureTriggered) { 22 | gesture->update_state(ev); 23 | } 24 | } 25 | } 26 | 27 | void IGestureManager::cancelTouchEventsOnAllWindows() { 28 | if (!this->inhibitTouchEvents) { 29 | this->inhibitTouchEvents = true; 30 | this->sendCancelEventsToWindows(); 31 | } 32 | } 33 | bool IGestureManager::emitCompletedGesture(const CompletedGestureEvent& gev) { 34 | bool handled = this->handleCompletedGesture(gev); 35 | if (handled) { 36 | // FIXME: I'm trying to prevent swipe:1:r from triggering when edge:l:r triggers 37 | // this only prevents it when there is a valid edge swipe, is that fine? 38 | this->gestureTriggered = true; 39 | this->stopLongPressTimer(); 40 | } 41 | 42 | return handled; 43 | } 44 | 45 | bool IGestureManager::emitDragGesture(const DragGestureEvent& gev) { 46 | bool handled = this->handleDragGesture(gev); 47 | if (handled) { 48 | this->gestureTriggered = true; 49 | this->activeDragGesture = std::optional(gev); 50 | this->stopLongPressTimer(); 51 | } 52 | 53 | return handled; 54 | } 55 | 56 | bool IGestureManager::emitDragGestureEnd(const DragGestureEvent& gev) { 57 | if (this->activeDragGesture.has_value() && this->activeDragGesture->type == gev.type) { 58 | 59 | this->handleDragGestureEnd(gev); 60 | this->activeDragGesture = std::nullopt; 61 | return true; 62 | } 63 | return false; 64 | } 65 | 66 | // @return whether or not to inhibit further actions 67 | bool IGestureManager::onTouchDown(const wf::touch::gesture_event_t& ev) { 68 | // NOTE @m_sGestureState is used in gesture-completed callbacks 69 | // during touch down it must be updated before updating the gestures 70 | // in touch up and motion, it must be updated AFTER updating the 71 | // gestures 72 | this->m_sGestureState.update(ev); 73 | this->updateGestures(ev); 74 | 75 | if (this->activeDragGesture.has_value()) { 76 | this->dragGestureUpdate(ev); 77 | } 78 | 79 | return this->eventForwardingInhibited(); 80 | } 81 | 82 | bool IGestureManager::onTouchUp(const wf::touch::gesture_event_t& ev) { 83 | this->updateGestures(ev); 84 | this->m_sGestureState.update(ev); 85 | 86 | if (this->activeDragGesture.has_value()) { 87 | this->dragGestureUpdate(ev); 88 | } 89 | 90 | return this->eventForwardingInhibited(); 91 | } 92 | 93 | bool IGestureManager::onTouchMove(const wf::touch::gesture_event_t& ev) { 94 | this->updateGestures(ev); 95 | this->m_sGestureState.update(ev); 96 | 97 | if (this->activeDragGesture.has_value()) { 98 | this->dragGestureUpdate(ev); 99 | } 100 | 101 | return this->eventForwardingInhibited(); 102 | } 103 | 104 | GestureDirection IGestureManager::find_swipe_edges(wf::touch::point_t point, int edge_margin) { 105 | auto mon = this->getMonitorArea(); 106 | 107 | GestureDirection edge_directions = 0; 108 | 109 | if (point.x <= mon.x + edge_margin) { 110 | edge_directions |= GESTURE_DIRECTION_LEFT; 111 | } 112 | 113 | if (point.x >= mon.x + mon.w - edge_margin) { 114 | edge_directions |= GESTURE_DIRECTION_RIGHT; 115 | } 116 | 117 | if (point.y <= mon.y + edge_margin) { 118 | edge_directions |= GESTURE_DIRECTION_UP; 119 | } 120 | 121 | if (point.y >= mon.y + mon.h - edge_margin) { 122 | edge_directions |= GESTURE_DIRECTION_DOWN; 123 | } 124 | 125 | return edge_directions; 126 | } 127 | 128 | void IGestureManager::addTouchGesture(std::unique_ptr gesture) { 129 | this->m_vGestures.emplace_back(std::move(gesture)); 130 | } 131 | 132 | void IGestureManager::addMultiFingerGesture(const float* sensitivity, const int64_t* timeout) { 133 | auto swipe = std::make_unique(SWIPE_INCORRECT_DRAG_TOLERANCE, sensitivity, timeout); 134 | 135 | auto swipe_ptr = swipe.get(); 136 | 137 | auto swipe_and_emit = std::make_unique(std::move(swipe), [=, this]() { 138 | if (this->activeDragGesture.has_value()) { 139 | return; 140 | } 141 | const auto gesture = DragGestureEvent{DragGestureType::SWIPE, swipe_ptr->target_direction, 142 | static_cast(this->m_sGestureState.fingers.size())}; 143 | 144 | if (this->emitDragGesture(gesture)) { 145 | this->cancelTouchEventsOnAllWindows(); 146 | } 147 | }); 148 | 149 | auto swipe_liftoff = std::make_unique(); 150 | 151 | std::vector> swipe_actions; 152 | swipe_actions.emplace_back(std::move(swipe_and_emit)); 153 | swipe_actions.emplace_back(std::move(swipe_liftoff)); 154 | 155 | auto ack = [swipe_ptr, this]() { 156 | const auto drag = 157 | DragGestureEvent{DragGestureType::SWIPE, 0, static_cast(this->m_sGestureState.fingers.size())}; 158 | if (this->emitDragGestureEnd(drag)) { 159 | return; 160 | } else { 161 | const auto gesture = CompletedGestureEvent{CompletedGestureType::SWIPE, swipe_ptr->target_direction, 162 | static_cast(this->m_sGestureState.fingers.size())}; 163 | 164 | this->emitCompletedGesture(gesture); 165 | } 166 | }; 167 | auto cancel = [this]() { this->handleCancelledGesture(); }; 168 | 169 | this->addTouchGesture(std::make_unique(std::move(swipe_actions), ack, cancel)); 170 | } 171 | 172 | void IGestureManager::addMultiFingerTap(const float* sensitivity, const int64_t* timeout) { 173 | auto tap = std::make_unique(SWIPE_INCORRECT_DRAG_TOLERANCE, sensitivity, timeout); 174 | 175 | std::vector> tap_actions; 176 | tap_actions.emplace_back(std::move(tap)); 177 | 178 | auto ack = [this]() { 179 | const auto gesture = 180 | CompletedGestureEvent{CompletedGestureType::TAP, 0, static_cast(this->m_sGestureState.fingers.size())}; 181 | if (this->emitCompletedGesture(gesture)) { 182 | this->cancelTouchEventsOnAllWindows(); 183 | } 184 | }; 185 | auto cancel = [this]() { this->handleCancelledGesture(); }; 186 | 187 | this->addTouchGesture(std::make_unique(std::move(tap_actions), ack, cancel)); 188 | } 189 | 190 | void IGestureManager::addLongPress(const float* sensitivity, const int64_t* delay) { 191 | auto long_press_and_emit = std::make_unique( 192 | std::make_unique( 193 | SWIPE_INCORRECT_DRAG_TOLERANCE, sensitivity, delay, 194 | [this](uint32_t current_time, uint32_t delay) { this->updateLongPressTimer(current_time, delay); }), 195 | [this]() { 196 | if (this->activeDragGesture.has_value()) { 197 | return; 198 | } 199 | const auto gesture = DragGestureEvent{DragGestureType::LONG_PRESS, 0, 200 | static_cast(this->m_sGestureState.fingers.size())}; 201 | 202 | if (this->emitDragGesture(gesture)) { 203 | this->cancelTouchEventsOnAllWindows(); 204 | } 205 | }); 206 | 207 | auto lift_all = std::make_unique(); 208 | 209 | std::vector> long_press_actions; 210 | long_press_actions.emplace_back(std::move(long_press_and_emit)); 211 | long_press_actions.emplace_back(std::move(lift_all)); 212 | 213 | auto ack = [this]() { 214 | if (this->activeDragGesture.has_value()) { 215 | this->emitDragGestureEnd(this->activeDragGesture.value()); 216 | return; 217 | } else { 218 | const auto gesture = CompletedGestureEvent{CompletedGestureType::LONG_PRESS, 0, 219 | static_cast(this->m_sGestureState.fingers.size())}; 220 | 221 | this->emitCompletedGesture(gesture); 222 | }; 223 | }; 224 | auto cancel = [this]() { 225 | this->stopLongPressTimer(); 226 | this->handleCancelledGesture(); 227 | }; 228 | 229 | this->addTouchGesture(std::make_unique(std::move(long_press_actions), ack, cancel)); 230 | } 231 | 232 | void IGestureManager::addEdgeSwipeGesture(const float* sensitivity, const int64_t* timeout, 233 | const long int* edge_margin) { 234 | auto edge = std::make_unique(SWIPE_INCORRECT_DRAG_TOLERANCE, sensitivity, timeout); 235 | auto edge_ptr = edge.get(); 236 | auto edge_drag_begin = std::make_unique(std::move(edge), [=, this]() { 237 | auto origin_edges = this->find_swipe_edges(m_sGestureState.get_center().origin, *edge_margin); 238 | 239 | if (origin_edges == 0) { 240 | return; 241 | } 242 | auto direction = edge_ptr->target_direction; 243 | auto gesture = DragGestureEvent{DragGestureType::EDGE_SWIPE, direction, edge_ptr->finger_count, origin_edges}; 244 | if (this->emitDragGesture(gesture)) { 245 | this->cancelTouchEventsOnAllWindows(); 246 | } 247 | }); 248 | auto edge_release = std::make_unique(1, false); 249 | 250 | // TODO do I really need this: 251 | // edge->set_move_tolerance(SWIPE_INCORRECT_DRAG_TOLERANCE * *sensitivity); 252 | 253 | // The release action needs longer duration to handle the case where the 254 | // gesture is actually longer than the max distance. 255 | // TODO make this adjustable: 256 | // edge_release->set_duration(GESTURE_BASE_DURATION * 1.5 * *sensitivity); 257 | 258 | std::vector> edge_swipe_actions; 259 | edge_swipe_actions.emplace_back(std::move(edge_drag_begin)); 260 | edge_swipe_actions.emplace_back(std::move(edge_release)); 261 | 262 | auto ack = [edge_ptr, edge_margin, this]() { 263 | auto origin_edges = find_swipe_edges(m_sGestureState.get_center().origin, *edge_margin); 264 | auto direction = edge_ptr->target_direction; 265 | auto dragEvent = DragGestureEvent{ 266 | .type = DragGestureType::EDGE_SWIPE, 267 | .direction = direction, 268 | .finger_count = edge_ptr->finger_count, 269 | .edge_origin = origin_edges, 270 | }; 271 | 272 | if (this->emitDragGestureEnd(dragEvent)) { 273 | return; 274 | } 275 | 276 | if (origin_edges == 0) { 277 | return; 278 | } 279 | 280 | auto event = 281 | CompletedGestureEvent{CompletedGestureType::EDGE_SWIPE, direction, edge_ptr->finger_count, origin_edges}; 282 | 283 | this->emitCompletedGesture(event); 284 | }; 285 | auto cancel = [this]() { this->handleCancelledGesture(); }; 286 | 287 | auto gesture = std::make_unique(std::move(edge_swipe_actions), ack, cancel); 288 | this->addTouchGesture(std::move(gesture)); 289 | } 290 | -------------------------------------------------------------------------------- /src/gestures/Gestures.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CompletedGesture.hpp" 4 | #include "DragGesture.hpp" 5 | #include "Logger.hpp" 6 | #include "Shared.hpp" 7 | #include 8 | #include 9 | #include 10 | 11 | struct SMonitorArea { 12 | double x, y, w, h; 13 | }; 14 | 15 | /* 16 | * Interface; there's only @CGestures and the mock gesture manager for testing 17 | * that implements this 18 | * 19 | * New gesture_t are added with @addTouchGesture(). Callbacks are triggered 20 | * during updateGestures if all actions within a geture_t is completed (actions 21 | * are chained serially, i.e. one action must be "completed" before the next 22 | * can start "running") 23 | */ 24 | class IGestureManager { 25 | public: 26 | IGestureManager(std::unique_ptr logger) : logger(std::move(logger)) {} 27 | virtual ~IGestureManager() {} 28 | // @return whether this touch event should be blocked from forwarding to the 29 | // client window/surface 30 | bool onTouchDown(const wf::touch::gesture_event_t&); 31 | 32 | // @return whether this touch event should be blocked from forwarding to the 33 | // client window/surface 34 | bool onTouchUp(const wf::touch::gesture_event_t&); 35 | 36 | // @return whether this touch event should be blocked from forwarding to the 37 | // client window/surface 38 | bool onTouchMove(const wf::touch::gesture_event_t&); 39 | 40 | void addTouchGesture(std::unique_ptr gesture); 41 | void addMultiFingerGesture(const float* sensitivity, const int64_t* timeout); 42 | void addMultiFingerTap(const float* sensitivity, const int64_t* timeout); 43 | void addLongPress(const float* sensitivity, const int64_t* delay); 44 | void addEdgeSwipeGesture(const float* sensitivity, const int64_t* timeout, const long* edge_margin); 45 | 46 | std::optional getActiveDragGesture() const { 47 | return activeDragGesture; 48 | } 49 | 50 | // indicates whether events should be blocked from forwarding to client 51 | // windows/surfaces 52 | bool eventForwardingInhibited() const { 53 | return inhibitTouchEvents; 54 | }; 55 | 56 | protected: 57 | std::vector> m_vGestures; 58 | wf::touch::gesture_state_t m_sGestureState; 59 | 60 | GestureDirection find_swipe_edges(wf::touch::point_t point, int edge_margin); 61 | virtual SMonitorArea getMonitorArea() const = 0; 62 | 63 | // handles gesture events and returns whether or not the event is used. 64 | virtual bool handleCompletedGesture(const CompletedGestureEvent& gev) = 0; 65 | 66 | // called at the start of drag evetns and returns whether or not the event is used. 67 | virtual bool handleDragGesture(const DragGestureEvent& gev) = 0; 68 | 69 | // called on every touch event while a drag gesture is active 70 | virtual void dragGestureUpdate(const wf::touch::gesture_event_t&) = 0; 71 | 72 | // called at the end of a drag event 73 | virtual void handleDragGestureEnd(const DragGestureEvent& gev) = 0; 74 | 75 | // this function should cleanup after drag gestures 76 | virtual void handleCancelledGesture() = 0; 77 | 78 | virtual void updateLongPressTimer(uint32_t current_time, uint32_t delay) = 0; 79 | virtual void stopLongPressTimer() = 0; 80 | 81 | private: 82 | std::unique_ptr logger; 83 | bool inhibitTouchEvents; 84 | bool gestureTriggered; // A drag/completed gesture is triggered 85 | std::optional activeDragGesture; 86 | 87 | // this function is called when needed to send "cancel touch" events to 88 | // client windows/surfaces 89 | virtual void sendCancelEventsToWindows() = 0; 90 | 91 | bool emitCompletedGesture(const CompletedGestureEvent& gev); 92 | bool emitDragGesture(const DragGestureEvent& gev); 93 | bool emitDragGestureEnd(const DragGestureEvent& gev); 94 | 95 | void updateGestures(const wf::touch::gesture_event_t&); 96 | void cancelTouchEventsOnAllWindows(); 97 | }; 98 | -------------------------------------------------------------------------------- /src/gestures/Logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | class Logger { 5 | public: 6 | virtual ~Logger() {} 7 | virtual void debug(std::string s) = 0; 8 | }; 9 | -------------------------------------------------------------------------------- /src/gestures/Shared.cpp: -------------------------------------------------------------------------------- 1 | #include "Shared.hpp" 2 | 3 | std::string stringifyDirection(GestureDirection direction) { 4 | std::string bind; 5 | if (direction & GESTURE_DIRECTION_LEFT) { 6 | bind += 'l'; 7 | } 8 | 9 | if (direction & GESTURE_DIRECTION_RIGHT) { 10 | bind += 'r'; 11 | } 12 | 13 | if (direction & GESTURE_DIRECTION_UP) { 14 | bind += 'u'; 15 | } 16 | 17 | if (direction & GESTURE_DIRECTION_DOWN) { 18 | bind += 'd'; 19 | } 20 | 21 | return bind; 22 | } 23 | -------------------------------------------------------------------------------- /src/gestures/Shared.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | #include 4 | 5 | // Swipe params 6 | constexpr static double MIN_SWIPE_DISTANCE = 30; 7 | constexpr static double MAX_SWIPE_DISTANCE = 450; 8 | constexpr static double SWIPE_INCORRECT_DRAG_TOLERANCE = 150; 9 | 10 | // Pinch params 11 | constexpr static double PINCH_INCORRECT_DRAG_TOLERANCE = 200; 12 | constexpr static double PINCH_THRESHOLD = 1.5; 13 | 14 | // Hold params 15 | constexpr static double HOLD_INCORRECT_DRAG_TOLERANCE = 100; 16 | 17 | // General 18 | constexpr static double GESTURE_INITIAL_TOLERANCE = 40; 19 | constexpr static uint32_t GESTURE_BASE_DURATION = 400; 20 | 21 | constexpr static uint32_t SEND_CANCEL_EVENT_FINGER_COUNT = 3; 22 | 23 | // can be one of @eTouchGestureDirection or a combination of them 24 | using GestureDirection = uint32_t; 25 | 26 | enum TouchGestureDirection { 27 | /* Swipe-specific */ 28 | GESTURE_DIRECTION_LEFT = (1 << 0), 29 | GESTURE_DIRECTION_RIGHT = (1 << 1), 30 | GESTURE_DIRECTION_UP = (1 << 2), 31 | GESTURE_DIRECTION_DOWN = (1 << 3), 32 | /* Pinch-specific */ 33 | // GESTURE_DIRECTION_IN = (1 << 4), 34 | // GESTURE_DIRECTION_OUT = (1 << 5), 35 | }; 36 | 37 | std::string stringifyDirection(GestureDirection direction); 38 | -------------------------------------------------------------------------------- /src/gestures/meson.build: -------------------------------------------------------------------------------- 1 | gestures = static_library('gestures', 2 | 'Gestures.cpp', 3 | 'Shared.cpp', 4 | 'Actions.cpp', 5 | 'CompletedGesture.cpp', 6 | 'DragGesture.cpp', 7 | dependencies: [ 8 | wftouch, 9 | ]) 10 | 11 | subdir('test') 12 | -------------------------------------------------------------------------------- /src/gestures/test/CoutLogger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../Logger.hpp" 3 | #include 4 | 5 | class CoutLogger : public Logger { 6 | void debug(std::string s) override { 7 | std::cout << s << std::endl; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/gestures/test/MockGestureManager.cpp: -------------------------------------------------------------------------------- 1 | #include "MockGestureManager.hpp" 2 | #include 3 | 4 | #define CONFIG_SENSITIVITY 1.0 5 | 6 | bool CMockGestureManager::handleCompletedGesture(const CompletedGestureEvent& gev) { 7 | std::cout << "gesture triggered: " << gev.to_string() << "\n"; 8 | this->triggered = true; 9 | return true; 10 | } 11 | 12 | bool CMockGestureManager::handleDragGesture(const DragGestureEvent& gev) { 13 | std::cout << "drag started: " << gev.to_string() << "\n"; 14 | return this->handlesDragEvents; 15 | } 16 | 17 | void CMockGestureManager::dragGestureUpdate(const wf::touch::gesture_event_t& gev) { 18 | std::cout << "drag update" << std::endl; 19 | } 20 | 21 | void CMockGestureManager::handleDragGestureEnd(const DragGestureEvent& gev) { 22 | std::cout << "drag end: " << gev.to_string() << "\n"; 23 | this->dragEnded = true; 24 | } 25 | 26 | void CMockGestureManager::handleCancelledGesture() { 27 | std::cout << "gesture cancelled\n"; 28 | this->cancelled = true; 29 | } 30 | 31 | void CMockGestureManager::sendCancelEventsToWindows() { 32 | std::cout << "cancel touch on windows\n"; 33 | } 34 | -------------------------------------------------------------------------------- /src/gestures/test/MockGestureManager.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include "../Gestures.hpp" 3 | #include "CoutLogger.hpp" 4 | #include "wayfire/touch/touch.hpp" 5 | #include 6 | #include 7 | 8 | constexpr double MONITOR_X = 0; 9 | constexpr double MONITOR_Y = 0; 10 | constexpr double MONITOR_WIDTH = 1920; 11 | constexpr double MONITOR_HEIGHT = 1080; 12 | 13 | class Tester { 14 | public: 15 | static void testFindSwipeEdges(); 16 | }; 17 | 18 | class CMockGestureManager final : public IGestureManager { 19 | public: 20 | CMockGestureManager(bool handlesDragEvents) 21 | : IGestureManager(std::make_unique()), handlesDragEvents(handlesDragEvents) {} 22 | ~CMockGestureManager() {} 23 | 24 | // if set to true, handleDragGesture() will return true 25 | bool handlesDragEvents; 26 | 27 | bool triggered = false; 28 | bool cancelled = false; 29 | bool dragEnded = false; 30 | 31 | struct { 32 | double x, y; 33 | } mon_offset = {MONITOR_X, MONITOR_Y}; 34 | 35 | struct { 36 | double w, h; 37 | } mon_size = {MONITOR_WIDTH, MONITOR_HEIGHT}; 38 | 39 | // creates a gesture manager that handles all drag gestures 40 | static CMockGestureManager newDragHandler() { 41 | return CMockGestureManager(true); 42 | } 43 | 44 | // creates a gesture manager that ignores drag gesture events 45 | static CMockGestureManager newCompletedGestureOnlyHandler() { 46 | return CMockGestureManager(false); 47 | } 48 | 49 | void resetTestResults() { 50 | triggered = false; 51 | cancelled = false; 52 | dragEnded = false; 53 | } 54 | 55 | auto getGestureAt(int index) const { 56 | return &this->m_vGestures.at(index); 57 | } 58 | 59 | wf::touch::point_t getLastPositionOfFinger(int id) { 60 | auto pos = &this->m_sGestureState.fingers[id].current; 61 | return {pos->x, pos->y}; 62 | } 63 | 64 | bool handleCompletedGesture(const CompletedGestureEvent& gev) override; 65 | bool handleDragGesture(const DragGestureEvent& gev) override; 66 | void dragGestureUpdate(const wf::touch::gesture_event_t&) override; 67 | void handleDragGestureEnd(const DragGestureEvent& gev) override; 68 | void handleCancelledGesture() override; 69 | 70 | void updateLongPressTimer(uint32_t current_time, uint32_t delay) override {} 71 | void stopLongPressTimer() override {} 72 | 73 | protected: 74 | SMonitorArea getMonitorArea() const override { 75 | return SMonitorArea{this->mon_offset.x, this->mon_offset.y, this->mon_size.w, this->mon_size.h}; 76 | } 77 | 78 | private: 79 | void sendCancelEventsToWindows() override; 80 | friend Tester; 81 | }; 82 | -------------------------------------------------------------------------------- /src/gestures/test/README.md: -------------------------------------------------------------------------------- 1 | Used for testing behavior of `wf-touch` 2 | -------------------------------------------------------------------------------- /src/gestures/test/meson.build: -------------------------------------------------------------------------------- 1 | doctest = dependency('doctest', required: get_option('tests')) 2 | if doctest.found() 3 | test_exe = executable('test-gestures', 4 | 'MockGestureManager.cpp', 5 | 'test.cpp', 6 | link_with: gestures, 7 | dependencies: [ 8 | wftouch, 9 | doctest 10 | ] 11 | ) 12 | 13 | test('test gestures', test_exe) 14 | endif 15 | -------------------------------------------------------------------------------- /src/gestures/test/test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 3 | #include 4 | 5 | #include "MockGestureManager.hpp" 6 | #include "wayfire/touch/touch.hpp" 7 | #include 8 | 9 | constexpr float SENSITIVITY = 1.0; 10 | constexpr int64_t LONG_PRESS_DELAY = GESTURE_BASE_DURATION; 11 | constexpr long int EDGE_MARGIN = 10; 12 | 13 | void Tester::testFindSwipeEdges() { 14 | using Test = struct { 15 | wf::touch::point_t origin; 16 | GestureDirection result; 17 | }; 18 | const auto L = GESTURE_DIRECTION_LEFT; 19 | const auto R = GESTURE_DIRECTION_RIGHT; 20 | const auto U = GESTURE_DIRECTION_UP; 21 | const auto D = GESTURE_DIRECTION_DOWN; 22 | 23 | Test tests[] = { 24 | {{MONITOR_X + 10, MONITOR_Y + 10}, U | L}, 25 | {{MONITOR_X, MONITOR_Y + 11}, L}, 26 | {{MONITOR_X + 11, MONITOR_Y}, U}, 27 | {{MONITOR_X + 11, MONITOR_Y + 11}, 0}, 28 | {{MONITOR_X + MONITOR_WIDTH, MONITOR_Y + MONITOR_HEIGHT}, D | R}, 29 | {{MONITOR_X + MONITOR_WIDTH - 11, MONITOR_Y + MONITOR_HEIGHT}, D}, 30 | {{MONITOR_X + MONITOR_WIDTH, MONITOR_Y + MONITOR_HEIGHT - 11}, R}, 31 | {{MONITOR_X + MONITOR_WIDTH - 11, MONITOR_Y + MONITOR_HEIGHT - 11}, 0}, 32 | }; 33 | 34 | auto mockGM = CMockGestureManager::newCompletedGestureOnlyHandler(); 35 | for (auto& test : tests) { 36 | CHECK_EQ(mockGM.find_swipe_edges(test.origin, EDGE_MARGIN), test.result); 37 | } 38 | } 39 | 40 | enum class ExpectResultType { 41 | COMPLETED, 42 | DRAG_TRIGGERED, 43 | DRAG_ENDED, 44 | CANCELLED, 45 | CHECK_PROGRESS, 46 | NOTHING_HAPPENED, 47 | }; 48 | 49 | struct ExpectResult { 50 | ExpectResultType type; 51 | 52 | // variables below only used when type == CHECK_PROGRESS 53 | float progress = 0.0; 54 | 55 | // which of the m_vGestures to use 56 | // usually the first (0) in tests 57 | int gesture_index = 0; 58 | }; 59 | 60 | // NOTE each event implicitly means the gesture has not been triggered/cancelled 61 | // yet 62 | void ProcessEvent(CMockGestureManager& gm, const wf::touch::gesture_event_t& ev) { 63 | CHECK_FALSE(gm.triggered); 64 | CHECK_FALSE(gm.cancelled); 65 | switch (ev.type) { 66 | case wf::touch::EVENT_TYPE_TOUCH_DOWN: 67 | gm.onTouchDown(ev); 68 | break; 69 | case wf::touch::EVENT_TYPE_TOUCH_UP: 70 | gm.onTouchUp(ev); 71 | break; 72 | case wf::touch::EVENT_TYPE_MOTION: 73 | gm.onTouchMove(ev); 74 | break; 75 | } 76 | } 77 | 78 | void ProcessEvents(CMockGestureManager& gm, ExpectResult expect, 79 | const std::vector& events) { 80 | for (const auto& ev : events) { 81 | ProcessEvent(gm, ev); 82 | } 83 | 84 | switch (expect.type) { 85 | case ExpectResultType::COMPLETED: 86 | CHECK(gm.triggered); 87 | break; 88 | case ExpectResultType::DRAG_TRIGGERED: 89 | CHECK(gm.getActiveDragGesture().has_value()); 90 | CHECK(gm.eventForwardingInhibited()); 91 | break; 92 | case ExpectResultType::DRAG_ENDED: 93 | CHECK(gm.dragEnded); 94 | CHECK(gm.eventForwardingInhibited()); 95 | break; 96 | case ExpectResultType::CANCELLED: 97 | CHECK(gm.cancelled); 98 | break; 99 | case ExpectResultType::NOTHING_HAPPENED: 100 | CHECK(!gm.cancelled); 101 | CHECK(!gm.dragEnded); 102 | CHECK(!gm.getActiveDragGesture().has_value()); 103 | case ExpectResultType::CHECK_PROGRESS: 104 | const auto got = gm.getGestureAt(expect.gesture_index)->get()->get_progress(); 105 | // fuck floating point math 106 | CHECK(std::abs(got - expect.progress) < 1e-5); 107 | break; 108 | } 109 | } 110 | 111 | using TouchEvent = wf::touch::gesture_event_t; 112 | using wf::touch::point_t; 113 | 114 | TEST_CASE("Multifinger: block touch events to client surfaces when more than a " 115 | "certain number of fingers touch down." * 116 | // multi-finger used to cancel touch events on 3 finger touch down 117 | // currently removed but may bring it back 118 | doctest::should_fail()) { 119 | std::cout << " ==== stdout:" << std::endl; 120 | auto gm = CMockGestureManager::newCompletedGestureOnlyHandler(); 121 | gm.addMultiFingerGesture(&SENSITIVITY, &LONG_PRESS_DELAY); 122 | const std::vector events{ 123 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, 124 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 120, 1, {500, 300}}, 125 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 140, 2, {550, 290}}, 126 | }; 127 | ProcessEvents(gm, {.type = ExpectResultType::CHECK_PROGRESS, .progress = 1.0 / 3.0}, events); 128 | 129 | CHECK(gm.eventForwardingInhibited()); 130 | } 131 | 132 | TEST_CASE("Swipe Drag: Start drag upon moving more than the threshold") { 133 | std::cout << " ==== stdout:" << std::endl; 134 | auto gm = CMockGestureManager::newDragHandler(); 135 | gm.addMultiFingerGesture(&SENSITIVITY, &LONG_PRESS_DELAY); 136 | const std::vector events{ 137 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, 138 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 1, {500, 300}}, 139 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 2, {550, 290}}, 140 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {0, 290}}, 141 | }; 142 | ProcessEvents(gm, {.type = ExpectResultType::DRAG_TRIGGERED}, events); 143 | } 144 | 145 | TEST_CASE("Swipe Drag: Cancel 3 finger swipe due to moving too much before " 146 | "adding new finger, but not enough to trigger 3 finger swipe first") { 147 | std::cout << " ==== stdout:" << std::endl; 148 | auto gm = CMockGestureManager::newDragHandler(); 149 | gm.addMultiFingerGesture(&SENSITIVITY, &LONG_PRESS_DELAY); 150 | const std::vector events{ 151 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 1, {500, 300}}, 152 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 2, {401, 290}}, {wf::touch::EVENT_TYPE_MOTION, 110, 0, {409, 290}}, 153 | {wf::touch::EVENT_TYPE_MOTION, 110, 1, {459, 300}}, {wf::touch::EVENT_TYPE_TOUCH_DOWN, 120, 3, {600, 280}}, 154 | }; 155 | ProcessEvents(gm, {.type = ExpectResultType::CANCELLED}, events); 156 | } 157 | 158 | TEST_CASE("Swipe: Complete upon moving more than the threshold then lifting a " 159 | "finger") { 160 | std::cout << " ==== stdout:" << std::endl; 161 | auto gm = CMockGestureManager::newCompletedGestureOnlyHandler(); 162 | gm.addMultiFingerGesture(&SENSITIVITY, &LONG_PRESS_DELAY); 163 | 164 | const std::vector events{ 165 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, 166 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 1, {500, 300}}, 167 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 2, {550, 290}}, 168 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {0, 290}}, 169 | // TODO CHECK progress == 0.5 170 | {wf::touch::EVENT_TYPE_MOTION, 200, 1, {50, 300}}, 171 | {wf::touch::EVENT_TYPE_MOTION, 200, 2, {100, 290}}, 172 | {wf::touch::EVENT_TYPE_TOUCH_UP, 300, 0, {100, 290}}, 173 | }; 174 | ProcessEvents(gm, {.type = ExpectResultType::COMPLETED}, events); 175 | } 176 | 177 | TEST_CASE("Multi-finger Tap") { 178 | std::cout << " ==== stdout:" << std::endl; 179 | auto gm = CMockGestureManager::newCompletedGestureOnlyHandler(); 180 | gm.addMultiFingerTap(&SENSITIVITY, &LONG_PRESS_DELAY); 181 | 182 | const std::vector events{ 183 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, 184 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 105, 1, {500, 300}}, 185 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 110, 2, {550, 290}}, 186 | {wf::touch::EVENT_TYPE_TOUCH_UP, 120, 2, {550, 290}}, 187 | }; 188 | 189 | ProcessEvents(gm, {.type = ExpectResultType::COMPLETED}, events); 190 | } 191 | 192 | TEST_CASE("Multi-finger Tap: Timeout") { 193 | std::cout << " ==== stdout:" << std::endl; 194 | auto gm = CMockGestureManager::newCompletedGestureOnlyHandler(); 195 | gm.addMultiFingerTap(&SENSITIVITY, &LONG_PRESS_DELAY); 196 | 197 | const std::vector events{ 198 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, 199 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 105, 1, {500, 300}}, 200 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 110, 2, {550, 290}}, 201 | {wf::touch::EVENT_TYPE_TOUCH_UP, 510, 2, {550, 290}}, 202 | }; 203 | 204 | ProcessEvents(gm, {.type = ExpectResultType::CANCELLED}, events); 205 | } 206 | 207 | TEST_CASE("Multi-finger Tap: finger moved too much") { 208 | std::cout << " ==== stdout:" << std::endl; 209 | auto gm = CMockGestureManager::newCompletedGestureOnlyHandler(); 210 | gm.addMultiFingerTap(&SENSITIVITY, &LONG_PRESS_DELAY); 211 | 212 | const std::vector events{ 213 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, 214 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 105, 1, {500, 300}}, 215 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 110, 2, {550, 290}}, 216 | {wf::touch::EVENT_TYPE_MOTION, 120, 1, {650, 290}}, 217 | // {wf::touch::EVENT_TYPE_TOUCH_UP, 130, 2, {550, 290}}, 218 | }; 219 | 220 | ProcessEvents(gm, {.type = ExpectResultType::CANCELLED}, events); 221 | } 222 | 223 | TEST_CASE("Long press: begin drag") { 224 | std::cout << " ==== stdout:" << std::endl; 225 | auto gm = CMockGestureManager::newDragHandler(); 226 | gm.addLongPress(&SENSITIVITY, &LONG_PRESS_DELAY); 227 | 228 | const std::vector events{ 229 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, {wf::touch::EVENT_TYPE_TOUCH_DOWN, 105, 1, {500, 300}}, 230 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 110, 2, {550, 290}}, {wf::touch::EVENT_TYPE_MOTION, 200, 0, {460, 300}}, 231 | {wf::touch::EVENT_TYPE_MOTION, 300, 1, {510, 290}}, {wf::touch::EVENT_TYPE_MOTION, 511, 2, {560, 300}}, 232 | }; 233 | 234 | ProcessEvents(gm, {.type = ExpectResultType::DRAG_TRIGGERED}, events); 235 | } 236 | 237 | TEST_CASE("Long press and drag: full drag") { 238 | std::cout << " ==== stdout:" << std::endl; 239 | auto gm = CMockGestureManager::newDragHandler(); 240 | gm.addLongPress(&SENSITIVITY, &LONG_PRESS_DELAY); 241 | 242 | const std::vector events{ 243 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, {wf::touch::EVENT_TYPE_TOUCH_DOWN, 105, 1, {500, 300}}, 244 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 110, 2, {550, 290}}, {wf::touch::EVENT_TYPE_MOTION, 200, 0, {460, 300}}, 245 | {wf::touch::EVENT_TYPE_MOTION, 300, 1, {510, 290}}, {wf::touch::EVENT_TYPE_MOTION, 511, 2, {560, 300}}, 246 | {wf::touch::EVENT_TYPE_MOTION, 530, 0, {470, 310}}, {wf::touch::EVENT_TYPE_TOUCH_UP, 550, 2, {560, 300}}, 247 | {wf::touch::EVENT_TYPE_TOUCH_UP, 550, 0, {560, 300}}, {wf::touch::EVENT_TYPE_TOUCH_UP, 550, 1, {560, 300}}, 248 | }; 249 | 250 | ProcessEvents(gm, {.type = ExpectResultType::DRAG_ENDED}, events); 251 | } 252 | 253 | TEST_CASE("Long press and drag: touch down does nothing") { 254 | std::cout << " ==== stdout:" << std::endl; 255 | auto gm = CMockGestureManager::newDragHandler(); 256 | gm.addLongPress(&SENSITIVITY, &LONG_PRESS_DELAY); 257 | 258 | const std::vector events{ 259 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, {wf::touch::EVENT_TYPE_TOUCH_DOWN, 105, 1, {500, 300}}, 260 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 110, 2, {550, 290}}, {wf::touch::EVENT_TYPE_MOTION, 200, 0, {460, 300}}, 261 | {wf::touch::EVENT_TYPE_MOTION, 300, 1, {510, 290}}, {wf::touch::EVENT_TYPE_MOTION, 511, 2, {560, 300}}, 262 | {wf::touch::EVENT_TYPE_MOTION, 530, 0, {470, 310}}, {wf::touch::EVENT_TYPE_TOUCH_DOWN, 550, 3, {560, 300}}, 263 | }; 264 | 265 | ProcessEvents(gm, {.type = ExpectResultType::CHECK_PROGRESS, .progress = 0.5}, events); 266 | } 267 | 268 | TEST_CASE("Long press and drag: cancelled due to short hold duration") { 269 | std::cout << " ==== stdout:" << std::endl; 270 | auto gm = CMockGestureManager::newDragHandler(); 271 | gm.addLongPress(&SENSITIVITY, &LONG_PRESS_DELAY); 272 | 273 | const std::vector events{ 274 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, {wf::touch::EVENT_TYPE_TOUCH_DOWN, 105, 1, {500, 300}}, 275 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 110, 2, {550, 290}}, {wf::touch::EVENT_TYPE_MOTION, 200, 0, {460, 300}}, 276 | {wf::touch::EVENT_TYPE_MOTION, 300, 1, {510, 290}}, {wf::touch::EVENT_TYPE_MOTION, 350, 2, {560, 300}}, 277 | {wf::touch::EVENT_TYPE_MOTION, 400, 1, {510, 290}}, {wf::touch::EVENT_TYPE_TOUCH_UP, 500, 2, {560, 300}}, 278 | }; 279 | 280 | ProcessEvents(gm, {.type = ExpectResultType::CANCELLED}, events); 281 | } 282 | 283 | TEST_CASE("Long press and drag: cancelled due to too much movement") { 284 | std::cout << " ==== stdout:" << std::endl; 285 | auto gm = CMockGestureManager::newDragHandler(); 286 | gm.addLongPress(&SENSITIVITY, &LONG_PRESS_DELAY); 287 | 288 | const std::vector events{ 289 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {450, 290}}, 290 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 105, 1, {500, 300}}, 291 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 110, 2, {550, 290}}, 292 | {wf::touch::EVENT_TYPE_MOTION, 200, 1, {650, 290}}, 293 | }; 294 | 295 | ProcessEvents(gm, {.type = ExpectResultType::CANCELLED}, events); 296 | } 297 | 298 | TEST_CASE("Edge Swipe: Complete upon: \n" 299 | "1. touch down on edge of screen\n" 300 | "2. swiping more than the threshold, within the time limit, then\n" 301 | "3. lifting the finger, within the time limit.\n") { 302 | std::cout << " ==== stdout:" << std::endl; 303 | auto gm = CMockGestureManager::newCompletedGestureOnlyHandler(); 304 | gm.addEdgeSwipeGesture(&SENSITIVITY, &LONG_PRESS_DELAY, &EDGE_MARGIN); 305 | 306 | const std::vector events{ 307 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {5, 300}}, 308 | {wf::touch::EVENT_TYPE_MOTION, 150, 0, {250, 300}}, 309 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {455, 300}}, 310 | // TODO Check progress == 0.5 311 | {wf::touch::EVENT_TYPE_TOUCH_UP, 300, 0, {455, 300}}, 312 | }; 313 | ProcessEvents(gm, {.type = ExpectResultType::COMPLETED}, events); 314 | } 315 | 316 | // haven't gotten around to checking what's wrong 317 | TEST_CASE("Edge Swipe: Timeout during swiping phase") { 318 | std::cout << " ==== stdout:" << std::endl; 319 | auto gm = CMockGestureManager::newCompletedGestureOnlyHandler(); 320 | gm.addEdgeSwipeGesture(&SENSITIVITY, &LONG_PRESS_DELAY, &EDGE_MARGIN); 321 | 322 | const std::vector events{ 323 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {5, 300}}, 324 | {wf::touch::EVENT_TYPE_MOTION, 150, 0, {154, 300}}, 325 | {wf::touch::EVENT_TYPE_MOTION, 551, 0, {600, 300}}, 326 | }; 327 | ProcessEvents(gm, {.type = ExpectResultType::CANCELLED}, events); 328 | } 329 | 330 | TEST_CASE("Edge Swipe: Fail check at the end for not starting swipe from an edge\n" 331 | "1. touch down somewhere NOT considered edge.\n" 332 | "2. swipe more than the threshold, within the time limit, then\n" 333 | "3. lift the finger, within the time limit.\n" 334 | "The starting position of the swipe is checked at the end and should " 335 | "fail.") { 336 | std::cout << " ==== stdout:" << std::endl; 337 | auto gm = CMockGestureManager::newCompletedGestureOnlyHandler(); 338 | gm.addEdgeSwipeGesture(&SENSITIVITY, &LONG_PRESS_DELAY, &EDGE_MARGIN); 339 | 340 | const std::vector events{ 341 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {11, 300}}, 342 | {wf::touch::EVENT_TYPE_MOTION, 150, 0, {250, 300}}, 343 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {461, 300}}, 344 | // TODO Check progress == 0.5 345 | {wf::touch::EVENT_TYPE_TOUCH_UP, 300, 0, {461, 300}}, 346 | }; 347 | const ExpectResult expected_result = {ExpectResultType::CHECK_PROGRESS, 1.0}; 348 | ProcessEvents(gm, expected_result, events); 349 | } 350 | 351 | TEST_CASE("Edge Swipe Drag: begin") { 352 | std::cout << " ==== stdout:" << std::endl; 353 | auto gm = CMockGestureManager::newDragHandler(); 354 | gm.addEdgeSwipeGesture(&SENSITIVITY, &LONG_PRESS_DELAY, &EDGE_MARGIN); 355 | 356 | const std::vector events{ 357 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {5, 300}}, 358 | {wf::touch::EVENT_TYPE_MOTION, 150, 0, {250, 300}}, 359 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {455, 300}}, 360 | }; 361 | const ExpectResult expected_result = {ExpectResultType::DRAG_TRIGGERED, 1.0}; 362 | ProcessEvents(gm, expected_result, events); 363 | } 364 | 365 | TEST_CASE("Edge Swipe Drag: emits drag end event") { 366 | auto gm = CMockGestureManager::newDragHandler(); 367 | gm.addEdgeSwipeGesture(&SENSITIVITY, &LONG_PRESS_DELAY, &EDGE_MARGIN); 368 | 369 | const std::vector events{ 370 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {5, 300}}, {wf::touch::EVENT_TYPE_MOTION, 150, 0, {250, 300}}, 371 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {455, 300}}, {wf::touch::EVENT_TYPE_MOTION, 250, 0, {600, 300}}, 372 | {wf::touch::EVENT_TYPE_TOUCH_UP, 300, 0, {700, 400}}, 373 | }; 374 | 375 | const ExpectResult expected_result = {ExpectResultType::DRAG_ENDED, 1.0}; 376 | ProcessEvents(gm, expected_result, events); 377 | } 378 | 379 | TEST_CASE("Edge Swipe: margins") { 380 | SUBCASE("custom margin: less than threshold triggers") { 381 | auto gm = CMockGestureManager::newDragHandler(); 382 | long margin = 20; 383 | gm.addEdgeSwipeGesture(&SENSITIVITY, &LONG_PRESS_DELAY, &margin); 384 | 385 | const std::vector events{ 386 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {19, 300}}, {wf::touch::EVENT_TYPE_MOTION, 150, 0, {250, 300}}, 387 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {455, 300}}, {wf::touch::EVENT_TYPE_MOTION, 250, 0, {600, 300}}, 388 | {wf::touch::EVENT_TYPE_TOUCH_UP, 300, 0, {700, 400}}, 389 | }; 390 | 391 | const ExpectResult expected_result = {ExpectResultType::DRAG_ENDED, 1.0}; 392 | ProcessEvents(gm, expected_result, events); 393 | } 394 | 395 | SUBCASE("with non-zero offset") { 396 | auto gm = CMockGestureManager::newDragHandler(); 397 | gm.mon_offset = {2000, 0}; 398 | long margin = 20; 399 | gm.addEdgeSwipeGesture(&SENSITIVITY, &LONG_PRESS_DELAY, &margin); 400 | 401 | const std::vector events{ 402 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {2019, 300}}, {wf::touch::EVENT_TYPE_MOTION, 150, 0, {250, 300}}, 403 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {2455, 300}}, {wf::touch::EVENT_TYPE_MOTION, 250, 0, {600, 300}}, 404 | {wf::touch::EVENT_TYPE_TOUCH_UP, 300, 0, {2700, 400}}, 405 | }; 406 | 407 | const ExpectResult expected_result = {ExpectResultType::DRAG_ENDED, 1.0}; 408 | ProcessEvents(gm, expected_result, events); 409 | } 410 | 411 | SUBCASE("custom margin: more than threshold does not trigger") { 412 | auto gm = CMockGestureManager::newDragHandler(); 413 | long margin = 20; 414 | gm.addEdgeSwipeGesture(&SENSITIVITY, &LONG_PRESS_DELAY, &margin); 415 | 416 | const std::vector events{ 417 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {21, 300}}, {wf::touch::EVENT_TYPE_MOTION, 150, 0, {250, 300}}, 418 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {455, 300}}, {wf::touch::EVENT_TYPE_MOTION, 250, 0, {600, 300}}, 419 | {wf::touch::EVENT_TYPE_TOUCH_UP, 300, 0, {700, 400}}, 420 | }; 421 | 422 | const ExpectResult expected_result = {ExpectResultType::NOTHING_HAPPENED, 1.0}; 423 | ProcessEvents(gm, expected_result, events); 424 | } 425 | } 426 | 427 | TEST_CASE("Edge swipe: block touch events") { 428 | std::cout << " ==== stdout:" << std::endl; 429 | auto gm = CMockGestureManager::newDragHandler(); 430 | gm.addEdgeSwipeGesture(&SENSITIVITY, &LONG_PRESS_DELAY, &EDGE_MARGIN); 431 | const std::vector events{ 432 | {wf::touch::EVENT_TYPE_TOUCH_DOWN, 100, 0, {10, 300}}, 433 | {wf::touch::EVENT_TYPE_MOTION, 200, 0, {455, 300}}, 434 | }; 435 | ProcessEvents(gm, {.type = ExpectResultType::CHECK_PROGRESS, .progress = 1.0 / 2.0}, events); 436 | 437 | CHECK(gm.eventForwardingInhibited()); 438 | } 439 | -------------------------------------------------------------------------------- /src/globals.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | inline HANDLE PHANDLE = nullptr; 6 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "GestureManager.hpp" 2 | #include "TouchVisualizer.hpp" 3 | #include "globals.hpp" 4 | #include "src/SharedDefs.hpp" 5 | #include "src/managers/HookSystemManager.hpp" 6 | #include "version.hpp" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | const CHyprColor s_pluginColor = {0x61 / 255.0f, 0xAF / 255.0f, 0xEF / 255.0f, 1.0f}; 22 | const CHyprColor error_color = {204. / 255.0, 2. / 255.0, 2. / 255.0, 1.0}; 23 | const std::string KEYWORD_HG_BIND = "hyprgrass-bind"; 24 | 25 | inline std::unique_ptr g_pVisualizer; 26 | 27 | void hkOnTouchDown(void* _, SCallbackInfo& cbinfo, std::any e) { 28 | auto ev = std::any_cast(e); 29 | 30 | static auto const VISUALIZE = 31 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:debug:visualize_touch") 32 | ->getDataStaticPtr(); 33 | 34 | if (**VISUALIZE) 35 | g_pVisualizer->onTouchDown(ev); 36 | cbinfo.cancelled = g_pGestureManager->onTouchDown(ev); 37 | } 38 | 39 | void hkOnTouchUp(void* _, SCallbackInfo& cbinfo, std::any e) { 40 | auto ev = std::any_cast(e); 41 | 42 | static auto const VISUALIZE = 43 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:debug:visualize_touch") 44 | ->getDataStaticPtr(); 45 | 46 | if (**VISUALIZE) 47 | g_pVisualizer->onTouchUp(ev); 48 | cbinfo.cancelled = g_pGestureManager->onTouchUp(ev); 49 | } 50 | 51 | void hkOnTouchMove(void* _, SCallbackInfo& cbinfo, std::any e) { 52 | auto ev = std::any_cast(e); 53 | 54 | static auto const VISUALIZE = 55 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:debug:visualize_touch") 56 | ->getDataStaticPtr(); 57 | 58 | if (**VISUALIZE) 59 | g_pVisualizer->onTouchMotion(ev); 60 | cbinfo.cancelled = g_pGestureManager->onTouchMove(ev); 61 | } 62 | 63 | static void onPreConfigReload() { 64 | g_pGestureManager->internalBinds.clear(); 65 | } 66 | 67 | void onRenderStage(eRenderStage stage) { 68 | static auto const VISUALIZE = 69 | (Hyprlang::INT* const*)HyprlandAPI::getConfigValue(PHANDLE, "plugin:touch_gestures:debug:visualize_touch") 70 | ->getDataStaticPtr(); 71 | 72 | if (stage == RENDER_LAST_MOMENT && **VISUALIZE) { 73 | g_pVisualizer->onRender(); 74 | } 75 | } 76 | 77 | SDispatchResult listInternalBinds(std::string) { 78 | Debug::log(LOG, "[hyprgrass] Listing internal binds:"); 79 | for (const auto& bind : g_pGestureManager->internalBinds) { 80 | Debug::log(LOG, "[hyprgrass] | gesture: {}", bind->key); 81 | Debug::log(LOG, "[hyprgrass] | dispatcher: {}", bind->handler); 82 | Debug::log(LOG, "[hyprgrass] | arg: {}", bind->arg); 83 | Debug::log(LOG, "[hyprgrass] | mouse: {}", bind->mouse); 84 | Debug::log(LOG, "[hyprgrass] | locked: {}", bind->locked); 85 | Debug::log(LOG, "[hyprgrass] |"); 86 | } 87 | return SDispatchResult{.success = true}; 88 | } 89 | 90 | SDispatchResult listHooks(std::string event) { 91 | Debug::log(LOG, "[hyprgrass] Listing hooks:"); 92 | 93 | if (event != "") { 94 | const auto* vec = g_pHookSystem->getVecForEvent(event); 95 | Debug::log(LOG, "[hyprgrass] listeners of {}: {}", event, vec->size()); 96 | return SDispatchResult{.success = true}; 97 | } 98 | 99 | const auto* vec = g_pHookSystem->getVecForEvent("hyprgrass:edgeBegin"); 100 | Debug::log(LOG, "[hyprgrass] | edgeBegin listeners: {}", vec->size()); 101 | 102 | vec = g_pHookSystem->getVecForEvent("hyprgrass:edgeUpdate"); 103 | Debug::log(LOG, "[hyprgrass] | edgeUpdate listeners: {}", vec->size()); 104 | 105 | vec = g_pHookSystem->getVecForEvent("hyprgrass:edgeEnd"); 106 | Debug::log(LOG, "[hyprgrass] | edgeEnd listeners: {}", vec->size()); 107 | return SDispatchResult{.success = true}; 108 | } 109 | 110 | Hyprlang::CParseResult onNewBind(const char* K, const char* V) { 111 | std::string v = V; 112 | auto vars = CVarList(v, 4); 113 | Hyprlang::CParseResult result; 114 | struct { 115 | bool mouse; 116 | bool locked; 117 | } flags = {}; 118 | 119 | if (vars.size() < 3) { 120 | result.setError("must have at least 3 fields: , , , [args]"); 121 | return result; 122 | } 123 | 124 | if (!vars[0].empty()) { 125 | result.setError("MODIFIER keys not currently supported"); 126 | return result; 127 | } 128 | 129 | const int prefix_size = std::size(KEYWORD_HG_BIND); 130 | for (char c : std::string(K).substr(prefix_size)) { 131 | switch (c) { 132 | case 'm': 133 | flags.mouse = true; 134 | break; 135 | case 'l': 136 | flags.locked = true; 137 | break; 138 | default: 139 | HyprlandAPI::addNotification(PHANDLE, std::string("ignoring invalid hyprgrass-bind flag: ") + c, 140 | error_color, 5000); 141 | } 142 | } 143 | 144 | const auto key = vars[1]; 145 | const auto dispatcher = flags.mouse ? "mouse" : vars[2]; 146 | const auto dispatcherArgs = flags.mouse ? vars[2] : vars[3]; 147 | 148 | g_pGestureManager->internalBinds.emplace_back(makeShared(SKeybind{ 149 | .key = key, 150 | .handler = dispatcher, 151 | .arg = dispatcherArgs, 152 | .locked = flags.locked, 153 | .mouse = flags.mouse, 154 | })); 155 | 156 | return result; 157 | } 158 | 159 | std::shared_ptr g_pTouchDownHook; 160 | std::shared_ptr g_pTouchUpHook; 161 | std::shared_ptr g_pTouchMoveHook; 162 | 163 | // Do NOT change this function. 164 | APICALL EXPORT std::string PLUGIN_API_VERSION() { 165 | return HYPRLAND_API_VERSION; 166 | } 167 | 168 | APICALL EXPORT PLUGIN_DESCRIPTION_INFO PLUGIN_INIT(HANDLE handle) { 169 | PHANDLE = handle; 170 | 171 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:touch_gestures:workspace_swipe_fingers", 172 | Hyprlang::CConfigValue((Hyprlang::INT)3)); 173 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:touch_gestures:workspace_swipe_edge", 174 | Hyprlang::CConfigValue((Hyprlang::STRING) "d")); 175 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:touch_gestures:sensitivity", 176 | Hyprlang::CConfigValue((Hyprlang::FLOAT)1.0)); 177 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:touch_gestures:long_press_delay", 178 | Hyprlang::CConfigValue((Hyprlang::INT)400)); 179 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:touch_gestures:edge_margin", 180 | Hyprlang::CConfigValue((Hyprlang::INT)10)); 181 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:touch_gestures:experimental:send_cancel", 182 | Hyprlang::CConfigValue((Hyprlang::INT)1)); 183 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:touch_gestures:resize_on_border_long_press", 184 | Hyprlang::CConfigValue((Hyprlang::INT)1)); 185 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:touch_gestures:emulate_touchpad_swipe", 186 | Hyprlang::CConfigValue((Hyprlang::INT)0)); 187 | HyprlandAPI::addConfigValue(PHANDLE, "plugin:touch_gestures:debug:visualize_touch", 188 | Hyprlang::CConfigValue((Hyprlang::INT)0)); 189 | 190 | HyprlandAPI::addConfigKeyword(PHANDLE, KEYWORD_HG_BIND, onNewBind, Hyprlang::SHandlerOptions{.allowFlags = true}); 191 | static auto P0 = HyprlandAPI::registerCallbackDynamic( 192 | PHANDLE, "preConfigReload", [&](void* self, SCallbackInfo& info, std::any data) { onPreConfigReload(); }); 193 | 194 | HyprlandAPI::addDispatcherV2(PHANDLE, "touchBind", [&](std::string args) { 195 | HyprlandAPI::addNotification( 196 | PHANDLE, "[hyprgrass] touchBind dispatcher deprecated, use the hyprgrass-bind keyword instead", 197 | CHyprColor(0.8, 0.2, 0.2, 1.0), 5000); 198 | g_pGestureManager->touchBindDispatcher(args); 199 | return SDispatchResult{ 200 | .success = true, 201 | }; 202 | }); 203 | 204 | HyprlandAPI::addDispatcherV2(PHANDLE, "hyprgrass:debug:binds", listInternalBinds); 205 | HyprlandAPI::addDispatcherV2(PHANDLE, "hyprgrass:debug:hooks", listHooks); 206 | 207 | const auto hlTargetVersion = GIT_COMMIT_HASH; 208 | const auto hlVersion = HyprlandAPI::getHyprlandVersion(PHANDLE); 209 | 210 | if (hlVersion.hash != hlTargetVersion) { 211 | HyprlandAPI::addNotification(PHANDLE, "Mismatched Hyprland version! check logs for details", 212 | CHyprColor(0.8, 0.7, 0.26, 1.0), 5000); 213 | Debug::log(ERR, "[hyprgrass] version mismatch!"); 214 | Debug::log(ERR, "[hyprgrass] | hyprgrass was built against: {}", hlTargetVersion); 215 | Debug::log(ERR, "[hyprgrass] | actual hyprland version: {}", hlVersion.hash); 216 | } 217 | 218 | static auto P1 = HyprlandAPI::registerCallbackDynamic(PHANDLE, "touchDown", hkOnTouchDown); 219 | static auto P2 = HyprlandAPI::registerCallbackDynamic(PHANDLE, "touchUp", hkOnTouchUp); 220 | static auto P3 = HyprlandAPI::registerCallbackDynamic(PHANDLE, "touchMove", hkOnTouchMove); 221 | static auto P4 = HyprlandAPI::registerCallbackDynamic( 222 | PHANDLE, "render", [](void*, SCallbackInfo, std::any arg) { onRenderStage(std::any_cast(arg)); }); 223 | 224 | HyprlandAPI::reloadConfig(); 225 | 226 | g_pGestureManager = std::make_unique(); 227 | g_pVisualizer = std::make_unique(); 228 | 229 | return {"hyprgrass", "Touchscreen gestures", "horriblename", HYPRGRASS_VERSION}; 230 | } 231 | 232 | APICALL EXPORT void PLUGIN_EXIT() { 233 | // idk if I should do this, but just in case 234 | g_pGestureManager.reset(); 235 | } 236 | -------------------------------------------------------------------------------- /src/meson.build: -------------------------------------------------------------------------------- 1 | version_header = configure_file( 2 | input: 'version.hpp.in', 3 | output: 'version.hpp', 4 | configuration: { 5 | 'HYPRGRASS_VERSION': meson.project_version() 6 | } 7 | ) 8 | 9 | if get_option('hyprgrass') 10 | subdir('gestures') 11 | 12 | shared_module('hyprgrass', 13 | 'main.cpp', 14 | 'GestureManager.cpp', 15 | 'VecSet.cpp', 16 | 'TouchVisualizer.cpp', 17 | cpp_args: ['-DWLR_USE_UNSTABLE'], 18 | link_with: [gestures], 19 | dependencies: [ 20 | wftouch, 21 | hyprland_deps, 22 | hyprland_headers 23 | ], 24 | install: true) 25 | endif 26 | -------------------------------------------------------------------------------- /src/version.hpp.in: -------------------------------------------------------------------------------- 1 | #define HYPRGRASS_VERSION "@HYPRGRASS_VERSION@" 2 | --------------------------------------------------------------------------------