├── LICENSE ├── README.md ├── meson.build ├── meson_options.txt ├── src ├── actions.cpp ├── math.cpp └── touch.cpp ├── test ├── action_test.cpp ├── basic_test.cpp ├── gesture_test.cpp ├── meson.build └── shared.hpp └── wayfire └── touch └── touch.hpp /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Wayfire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wf-touch 2 | 3 | Touchscreen gesture library 4 | 5 | # Acknowledgements 6 | 7 | The library's design has been heavily inspired by https://github.com/grahnen/libtouch, 8 | which has also been used as a reference for some implementation details at times. 9 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('wf-touch', ['cpp'], 2 | version : '0.0', 3 | meson_version: '>=0.47.0', 4 | default_options : ['cpp_std=c++17']) 5 | 6 | glm = dependency('glm', required: false) 7 | if not glm.found() and not meson.get_compiler('cpp').check_header('glm/glm.hpp') 8 | error('GLM not found, and directly using the header \'glm/glm.hpp\' is not possible.') 9 | endif 10 | 11 | wf_touch_inc_dirs = include_directories('.') 12 | install_headers([ 13 | 'wayfire/touch/touch.hpp'], 14 | subdir: 'wayfire/touch') 15 | 16 | wftouch_lib = static_library('wftouch', ['src/touch.cpp', 'src/actions.cpp', 'src/math.cpp'], 17 | dependencies: glm, install: true) 18 | 19 | wftouch = declare_dependency(link_with: wftouch_lib, 20 | include_directories: wf_touch_inc_dirs, dependencies: glm) 21 | 22 | doctest = dependency('doctest', required: get_option('tests')) 23 | 24 | if doctest.found() 25 | subdir('test') 26 | endif 27 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('tests', type: 'feature', value: 'auto', description: 'Enable unit tests') 2 | -------------------------------------------------------------------------------- /src/actions.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace wf::touch; 5 | /* -------------------------- Touch action ---------------------------------- */ 6 | wf::touch::touch_action_t::touch_action_t(int cnt_fingers, bool touch_down) 7 | { 8 | this->cnt_fingers = cnt_fingers; 9 | this->type = touch_down ? EVENT_TYPE_TOUCH_DOWN : EVENT_TYPE_TOUCH_UP; 10 | 11 | this->target.x = -1e9; 12 | this->target.y = -1e9; 13 | this->target.width = 2e9; 14 | this->target.height = 2e9; 15 | } 16 | 17 | wf::touch::touch_action_t& wf::touch::touch_action_t::set_target(const touch_target_t& target) 18 | { 19 | this->target = target; 20 | return *this; 21 | } 22 | 23 | static double find_max_delta(const gesture_state_t& state) 24 | { 25 | double max_length = 0; 26 | for (auto& f : state.fingers) 27 | { 28 | max_length = std::max(max_length, glm::length(f.second.delta())); 29 | } 30 | 31 | return max_length; 32 | } 33 | 34 | bool wf::touch::touch_action_t::exceeds_tolerance(const gesture_state_t& state) 35 | { 36 | return find_max_delta(state) > this->move_tolerance; 37 | } 38 | 39 | void wf::touch::touch_action_t::reset(uint32_t time) 40 | { 41 | gesture_action_t::reset(time); 42 | this->cnt_touch_events = 0; 43 | } 44 | 45 | action_status_t wf::touch::touch_action_t::update_state( 46 | const gesture_state_t& state, const gesture_event_t& event) 47 | { 48 | if (exceeds_tolerance(state)) 49 | { 50 | return ACTION_STATUS_CANCELLED; 51 | } 52 | 53 | switch (event.type) 54 | { 55 | case EVENT_TYPE_MOTION: 56 | return ACTION_STATUS_RUNNING; 57 | case EVENT_TYPE_TIMEOUT: 58 | return ACTION_STATUS_CANCELLED; 59 | 60 | case EVENT_TYPE_TOUCH_UP: // fallthrough 61 | case EVENT_TYPE_TOUCH_DOWN: 62 | if (this->type != event.type) 63 | { 64 | // down when we want up or vice versa 65 | return ACTION_STATUS_CANCELLED; 66 | } 67 | 68 | for (auto& f : state.fingers) 69 | { 70 | point_t relevant_point = (this->type == EVENT_TYPE_TOUCH_UP ? f.second.current : f.second.origin); 71 | if (!this->target.contains(relevant_point)) 72 | { 73 | return ACTION_STATUS_CANCELLED; 74 | } 75 | } 76 | 77 | this->cnt_touch_events++; 78 | if (this->cnt_touch_events == this->cnt_fingers) 79 | { 80 | return ACTION_STATUS_COMPLETED; 81 | } else 82 | { 83 | return ACTION_STATUS_RUNNING; 84 | } 85 | } 86 | 87 | return ACTION_STATUS_RUNNING; 88 | } 89 | 90 | /*- -------------------------- Hold action ---------------------------------- */ 91 | wf::touch::hold_action_t::hold_action_t(int32_t threshold) 92 | { 93 | set_duration(threshold); 94 | } 95 | 96 | action_status_t wf::touch::hold_action_t::update_state(const gesture_state_t& state, 97 | const gesture_event_t& event) 98 | { 99 | switch (event.type) 100 | { 101 | case EVENT_TYPE_MOTION: 102 | if (exceeds_tolerance(state)) 103 | { 104 | return ACTION_STATUS_CANCELLED; 105 | } else 106 | { 107 | return ACTION_STATUS_RUNNING; 108 | } 109 | case EVENT_TYPE_TIMEOUT: 110 | return ACTION_STATUS_COMPLETED; 111 | default: 112 | return ACTION_STATUS_CANCELLED; 113 | } 114 | } 115 | 116 | bool wf::touch::hold_action_t::exceeds_tolerance(const gesture_state_t& state) 117 | { 118 | return find_max_delta(state) > this->move_tolerance; 119 | } 120 | 121 | /*- -------------------------- Drag action ---------------------------------- */ 122 | wf::touch::drag_action_t::drag_action_t(uint32_t direction, double threshold) 123 | { 124 | this->direction = direction; 125 | this->threshold = threshold; 126 | } 127 | 128 | action_status_t wf::touch::drag_action_t::update_state(const gesture_state_t& state, 129 | const gesture_event_t& event) 130 | { 131 | if (event.type != EVENT_TYPE_MOTION) 132 | { 133 | return ACTION_STATUS_CANCELLED; 134 | } 135 | 136 | if (exceeds_tolerance(state)) 137 | { 138 | return ACTION_STATUS_CANCELLED; 139 | } 140 | 141 | const double dragged = state.get_center().get_drag_distance(this->direction); 142 | if (dragged >= this->threshold) 143 | { 144 | return ACTION_STATUS_COMPLETED; 145 | } else 146 | { 147 | return ACTION_STATUS_RUNNING; 148 | } 149 | } 150 | 151 | bool wf::touch::drag_action_t::exceeds_tolerance(const gesture_state_t& state) 152 | { 153 | for (auto& f : state.fingers) 154 | { 155 | if (f.second.get_incorrect_drag_distance(this->direction) > move_tolerance) 156 | { 157 | return true; 158 | } 159 | } 160 | 161 | return false; 162 | } 163 | 164 | /*- -------------------------- Pinch action ---------------------------------- */ 165 | wf::touch::pinch_action_t::pinch_action_t(double threshold) 166 | { 167 | this->threshold = threshold; 168 | } 169 | 170 | action_status_t wf::touch::pinch_action_t::update_state(const gesture_state_t& state, 171 | const gesture_event_t& event) 172 | { 173 | if (event.type != EVENT_TYPE_MOTION) 174 | { 175 | return ACTION_STATUS_CANCELLED; 176 | } 177 | 178 | if (exceeds_tolerance(state)) 179 | { 180 | return ACTION_STATUS_CANCELLED; 181 | } 182 | 183 | const double current_scale = state.get_pinch_scale(); 184 | if (((this->threshold < 1.0) && (current_scale <= threshold)) || 185 | ((this->threshold > 1.0) && (current_scale >= threshold))) 186 | { 187 | return ACTION_STATUS_COMPLETED; 188 | } 189 | 190 | return ACTION_STATUS_RUNNING; 191 | } 192 | 193 | bool wf::touch::pinch_action_t::exceeds_tolerance(const gesture_state_t& state) 194 | { 195 | return glm::length(state.get_center().delta()) > this->move_tolerance; 196 | } 197 | 198 | /*- -------------------------- Rotate action ---------------------------------- */ 199 | wf::touch::rotate_action_t::rotate_action_t(double threshold) 200 | { 201 | this->threshold = threshold; 202 | } 203 | 204 | action_status_t wf::touch::rotate_action_t::update_state(const gesture_state_t& state, 205 | const gesture_event_t& event) 206 | { 207 | if (event.type != EVENT_TYPE_MOTION) 208 | { 209 | return ACTION_STATUS_CANCELLED; 210 | } 211 | 212 | if (exceeds_tolerance(state)) 213 | { 214 | return ACTION_STATUS_CANCELLED; 215 | } 216 | 217 | const double current_scale = state.get_rotation_angle(); 218 | if (((this->threshold < 0.0) && (current_scale <= threshold)) || 219 | ((this->threshold > 0.0) && (current_scale >= threshold))) 220 | { 221 | return ACTION_STATUS_COMPLETED; 222 | } 223 | 224 | return ACTION_STATUS_RUNNING; 225 | } 226 | 227 | bool wf::touch::rotate_action_t::exceeds_tolerance(const gesture_state_t& state) 228 | { 229 | return glm::length(state.get_center().delta()) > this->move_tolerance; 230 | } 231 | -------------------------------------------------------------------------------- /src/math.cpp: -------------------------------------------------------------------------------- 1 | #define GLM_ENABLE_EXPERIMENTAL // for glm::orientedAngle 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #define _ << " " << 10 | #define debug(x) #x << " = " << (x) 11 | 12 | static constexpr double DIRECTION_TAN_THRESHOLD = 1.0 / 3.0; 13 | 14 | using namespace wf::touch; 15 | uint32_t wf::touch::finger_t::get_direction() const 16 | { 17 | double to_left = this->get_drag_distance(MOVE_DIRECTION_LEFT); 18 | double to_right = this->get_drag_distance(MOVE_DIRECTION_RIGHT); 19 | double to_up = this->get_drag_distance(MOVE_DIRECTION_UP); 20 | double to_down = this->get_drag_distance(MOVE_DIRECTION_DOWN); 21 | 22 | double horizontal = std::max(to_left, to_right); 23 | double vertical = std::max(to_up, to_down); 24 | 25 | uint32_t result = 0; 26 | if (to_left > 0 && to_left / vertical >= DIRECTION_TAN_THRESHOLD) 27 | { 28 | result |= MOVE_DIRECTION_LEFT; 29 | } else if (to_right > 0 && to_right / vertical >= DIRECTION_TAN_THRESHOLD) 30 | { 31 | result |= MOVE_DIRECTION_RIGHT; 32 | } 33 | 34 | if (to_up > 0 && to_up / horizontal >= DIRECTION_TAN_THRESHOLD) 35 | { 36 | result |= MOVE_DIRECTION_UP; 37 | } else if (to_down > 0 && to_down / horizontal >= DIRECTION_TAN_THRESHOLD) 38 | { 39 | result |= MOVE_DIRECTION_DOWN; 40 | } 41 | 42 | return result; 43 | } 44 | 45 | /** Get normal vector in direction */ 46 | static point_t get_dir_nv(uint32_t direction) 47 | { 48 | assert((direction != 0) && ((direction & 0b1111) == direction)); 49 | 50 | point_t dir = {0, 0}; 51 | if (direction & MOVE_DIRECTION_LEFT) 52 | { 53 | dir.x = -1; 54 | } 55 | else if (direction & MOVE_DIRECTION_RIGHT) 56 | { 57 | dir.x = 1; 58 | } 59 | if (direction & MOVE_DIRECTION_UP) 60 | { 61 | dir.y = -1; 62 | } 63 | else if (direction & MOVE_DIRECTION_DOWN) 64 | { 65 | dir.y = 1; 66 | } 67 | 68 | return dir; 69 | } 70 | 71 | double wf::touch::finger_t::get_drag_distance(uint32_t direction) const 72 | { 73 | const auto normal = get_dir_nv(direction); 74 | const auto delta = this->delta(); 75 | 76 | /* grahm-schmidt */ 77 | const double amount_alongside_dir = glm::dot(delta, normal) / glm::dot(normal, normal); 78 | if (amount_alongside_dir >= 0) 79 | { 80 | return glm::length(amount_alongside_dir * normal); 81 | } 82 | 83 | return 0; 84 | } 85 | 86 | double wf::touch::finger_t::get_incorrect_drag_distance(uint32_t direction) const 87 | { 88 | const auto normal = get_dir_nv(direction); 89 | const auto delta = this->delta(); 90 | 91 | /* grahm-schmidt */ 92 | double amount_alongside_dir = glm::dot(delta, normal) / glm::dot(normal, normal); 93 | if (amount_alongside_dir < 0) 94 | { 95 | /* Drag in opposite direction */ 96 | return glm::length(delta); 97 | } 98 | 99 | const auto residual = delta - normal * amount_alongside_dir; 100 | return glm::length(residual); 101 | } 102 | 103 | double wf::touch::gesture_state_t::get_pinch_scale() const 104 | { 105 | auto center = get_center(); 106 | double old_dist = 0; 107 | double new_dist = 0; 108 | 109 | for (const auto& f : fingers) 110 | { 111 | old_dist += glm::length(f.second.origin - center.origin); 112 | new_dist += glm::length(f.second.current - center.current); 113 | } 114 | 115 | old_dist /= fingers.size(); 116 | new_dist /= fingers.size(); 117 | return new_dist / old_dist; 118 | } 119 | 120 | double wf::touch::gesture_state_t::get_rotation_angle() const 121 | { 122 | auto center = get_center(); 123 | 124 | double angle_sum = 0; 125 | for (const auto& f : fingers) 126 | { 127 | auto v1 = glm::normalize(f.second.origin - center.origin); 128 | auto v2 = glm::normalize(f.second.current - center.current); 129 | angle_sum += glm::orientedAngle(v1, v2); 130 | } 131 | 132 | angle_sum /= fingers.size(); 133 | return angle_sum; 134 | } 135 | -------------------------------------------------------------------------------- /src/touch.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | using namespace wf::touch; 4 | 5 | point_t wf::touch::finger_t::delta() const 6 | { 7 | return this->current - this->origin; 8 | } 9 | 10 | finger_t wf::touch::gesture_state_t::get_center() const 11 | { 12 | finger_t center; 13 | center.origin = {0, 0}; 14 | center.current = {0, 0}; 15 | 16 | for (auto& f : this->fingers) 17 | { 18 | center.origin += f.second.origin; 19 | center.current += f.second.current; 20 | } 21 | 22 | center.origin /= this->fingers.size(); 23 | center.current /= this->fingers.size(); 24 | return center; 25 | } 26 | 27 | void wf::touch::gesture_state_t::update(const gesture_event_t& event) 28 | { 29 | switch (event.type) 30 | { 31 | case EVENT_TYPE_TOUCH_DOWN: 32 | fingers[event.finger].origin = event.pos; 33 | // fallthrough 34 | case EVENT_TYPE_MOTION: 35 | fingers[event.finger].current = event.pos; 36 | break; 37 | case EVENT_TYPE_TOUCH_UP: 38 | fingers.erase(event.finger); 39 | break; 40 | default: 41 | break; 42 | } 43 | } 44 | 45 | void wf::touch::gesture_state_t::reset_origin() 46 | { 47 | for (auto& f : fingers) 48 | { 49 | f.second.origin = f.second.current; 50 | } 51 | } 52 | 53 | wf::touch::gesture_action_t& wf::touch::gesture_action_t::set_duration(uint32_t duration) 54 | { 55 | this->duration = duration; 56 | return *this; 57 | } 58 | 59 | std::optional wf::touch::gesture_action_t::get_duration() const 60 | { 61 | return this->duration; 62 | } 63 | 64 | void wf::touch::gesture_action_t::reset(uint32_t time) 65 | { 66 | this->start_time = time; 67 | } 68 | 69 | bool wf::touch::touch_target_t::contains(const point_t& pt) const 70 | { 71 | return x <= pt.x && pt.x < x + width && 72 | y <= pt.y && pt.y < y + height; 73 | } 74 | 75 | class wf::touch::gesture_t::impl 76 | { 77 | public: 78 | gesture_callback_t completed; 79 | gesture_callback_t cancelled; 80 | 81 | std::vector> actions; 82 | size_t current_action = 0; 83 | action_status_t status = ACTION_STATUS_CANCELLED; 84 | 85 | gesture_state_t finger_state; 86 | std::unique_ptr timer; 87 | 88 | void start_gesture(uint32_t time) 89 | { 90 | status = ACTION_STATUS_RUNNING; 91 | finger_state.fingers.clear(); 92 | current_action = 0; 93 | actions[0]->reset(time); 94 | start_timer(); 95 | } 96 | 97 | void start_timer() 98 | { 99 | if (auto dur = actions[current_action]->get_duration()) 100 | { 101 | timer->set_timeout(*dur, [=] () 102 | { 103 | update_state(gesture_event_t{.type = EVENT_TYPE_TIMEOUT}); 104 | }); 105 | } 106 | } 107 | 108 | void update_state(const gesture_event_t& event) 109 | { 110 | if (status != ACTION_STATUS_RUNNING) 111 | { 112 | // nothing to do 113 | return; 114 | } 115 | 116 | auto& idx = current_action; 117 | 118 | auto old_finger_state = finger_state; 119 | finger_state.update(event); 120 | 121 | auto next_action = [&] () -> bool 122 | { 123 | timer->reset(); 124 | ++idx; 125 | if (idx < actions.size()) 126 | { 127 | actions[idx]->reset(event.time); 128 | finger_state.reset_origin(); 129 | start_timer(); 130 | return true; 131 | } 132 | 133 | return false; 134 | }; 135 | 136 | action_status_t pending_status = actions[idx]->update_state(finger_state, event); 137 | switch (pending_status) 138 | { 139 | case ACTION_STATUS_RUNNING: 140 | return; // nothing more to do 141 | 142 | case ACTION_STATUS_CANCELLED: 143 | this->status = ACTION_STATUS_CANCELLED; 144 | timer->reset(); 145 | cancelled(); 146 | return; 147 | 148 | case ACTION_STATUS_COMPLETED: 149 | bool has_next = next_action(); 150 | if (!has_next) 151 | { 152 | this->status = ACTION_STATUS_COMPLETED; 153 | completed(); 154 | return; 155 | } 156 | } 157 | } 158 | }; 159 | 160 | void wf::touch::gesture_t::set_timer(std::unique_ptr timer) 161 | { 162 | priv->timer = std::move(timer); 163 | } 164 | 165 | wf::touch::gesture_t::gesture_t(std::vector> actions, 166 | gesture_callback_t completed, gesture_callback_t cancelled) 167 | { 168 | this->priv = std::make_unique(); 169 | priv->actions = std::move(actions); 170 | priv->completed = completed; 171 | priv->cancelled = cancelled; 172 | } 173 | 174 | wf::touch::gesture_t::gesture_t(gesture_t&& other) 175 | { 176 | this->priv = std::move(other.priv); 177 | } 178 | 179 | wf::touch::gesture_t& wf::touch::gesture_t::operator=(gesture_t&& other) 180 | { 181 | this->priv = std::move(other.priv); 182 | return *this; 183 | } 184 | 185 | wf::touch::gesture_t::~gesture_t() = default; 186 | 187 | double wf::touch::gesture_t::get_progress() const 188 | { 189 | if (priv->status == ACTION_STATUS_CANCELLED) 190 | { 191 | return 0.0; 192 | } 193 | 194 | return 1.0 * priv->current_action / priv->actions.size(); 195 | } 196 | 197 | void wf::touch::gesture_t::update_state(const gesture_event_t& event) 198 | { 199 | assert(priv->timer); 200 | assert(!priv->actions.empty()); 201 | 202 | priv->update_state(event); 203 | } 204 | 205 | wf::touch::action_status_t wf::touch::gesture_t::get_status() const 206 | { 207 | return priv->status; 208 | } 209 | 210 | void wf::touch::gesture_t::reset(uint32_t time) 211 | { 212 | assert(priv->timer); 213 | assert(!priv->actions.empty()); 214 | 215 | if (priv->status == ACTION_STATUS_RUNNING) 216 | { 217 | return; 218 | } 219 | 220 | priv->start_gesture(time); 221 | } 222 | 223 | wf::touch::gesture_builder_t::gesture_builder_t() {} 224 | 225 | wf::touch::gesture_builder_t& wf::touch::gesture_builder_t::on_completed(gesture_callback_t callback) 226 | { 227 | this->_on_completed = callback; 228 | return *this; 229 | } 230 | 231 | wf::touch::gesture_builder_t& wf::touch::gesture_builder_t::on_cancelled(gesture_callback_t callback) 232 | { 233 | this->_on_cancelled = callback; 234 | return *this; 235 | } 236 | 237 | wf::touch::gesture_t wf::touch::gesture_builder_t::build() 238 | { 239 | return gesture_t(std::move(actions), _on_completed, _on_cancelled); 240 | } 241 | -------------------------------------------------------------------------------- /test/action_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | #include "shared.hpp" 4 | 5 | TEST_CASE("touch_action_t") 6 | { 7 | touch_action_t touch_down{2, true}; 8 | touch_down.set_target({0, 0, 10, 10}); 9 | touch_down.set_duration(150); 10 | touch_down.set_move_tolerance(5); 11 | 12 | gesture_event_t event_down; 13 | event_down.type = EVENT_TYPE_TOUCH_DOWN; 14 | event_down.time = 75; 15 | 16 | // check normal operation, with tolerance 17 | gesture_state_t state; 18 | state.fingers[0] = finger_2p(0, 0, 0, 0); 19 | touch_down.reset(0); 20 | CHECK(touch_down.update_state(state, event_down) == ACTION_STATUS_RUNNING); 21 | 22 | gesture_event_t motion; 23 | motion.type = EVENT_TYPE_MOTION; 24 | motion.finger = 0; 25 | motion.time = 100; 26 | motion.pos = {1, 1}; 27 | state.fingers[0] = finger_2p(0, 0, 1, 1); 28 | CHECK(touch_down.update_state(state, motion) == ACTION_STATUS_RUNNING); 29 | 30 | state.fingers[1] = finger_in_dir(2, 2); 31 | event_down.finger = 2; 32 | event_down.pos = {2, 2}; 33 | event_down.time = 150; 34 | CHECK(touch_down.update_state(state, event_down) == ACTION_STATUS_COMPLETED); 35 | 36 | // check outside of bounds 37 | state.fingers[0] = finger_2p(15, 15, 20, 20); 38 | touch_down.reset(0); 39 | CHECK(touch_down.update_state(state, event_down) == ACTION_STATUS_CANCELLED); 40 | state.fingers[0] = finger_2p(0, 0, 0, 0); 41 | 42 | // check timeout 43 | touch_down.reset(0); 44 | CHECK(touch_down.update_state(state, gesture_event_t{.type = EVENT_TYPE_TIMEOUT}) == 45 | ACTION_STATUS_CANCELLED); 46 | 47 | touch_action_t touch_up{2, false}; 48 | gesture_event_t event_up; 49 | event_up.type = EVENT_TYPE_TOUCH_UP; 50 | event_up.time = 150; 51 | 52 | // start touch up action 53 | state.fingers[1] = finger_2p(2, 2, 3, 3); 54 | touch_up.reset(0); 55 | CHECK(touch_up.update_state(state, event_up) == ACTION_STATUS_RUNNING); 56 | 57 | // complete it 58 | state.fingers.erase(1); 59 | CHECK(touch_up.update_state(state, event_up) == ACTION_STATUS_COMPLETED); 60 | 61 | // check tolerance exceeded 62 | state.fingers[1] = finger_2p(2, 2, 2, 3); 63 | touch_up.set_move_tolerance(0); 64 | touch_up.reset(0); 65 | CHECK(touch_up.update_state(state, event_up) == ACTION_STATUS_CANCELLED); 66 | } 67 | 68 | TEST_CASE("wf::touch::hold_action_t") 69 | { 70 | hold_action_t hold{50}; 71 | hold.set_move_tolerance(1); 72 | 73 | gesture_state_t state; 74 | state.fingers[0] = finger_in_dir(1, 0); 75 | gesture_event_t ev; 76 | 77 | // check ok state 78 | hold.reset(0); 79 | ev.time = 49; 80 | ev.type = EVENT_TYPE_MOTION; 81 | CHECK(hold.update_state(state, ev) == ACTION_STATUS_RUNNING); 82 | CHECK(hold.update_state(state, gesture_event_t{.type = EVENT_TYPE_TIMEOUT}) == ACTION_STATUS_COMPLETED); 83 | 84 | // check finger breaks action 85 | hold.reset(0); 86 | ev.type = EVENT_TYPE_TOUCH_UP; 87 | ev.time = 49; 88 | CHECK(hold.update_state(state, ev) == ACTION_STATUS_CANCELLED); 89 | 90 | // check too much movement 91 | state.fingers[0] = finger_in_dir(2, 0); 92 | ev.time = 49; 93 | hold.reset(0); 94 | CHECK(hold.update_state(state, ev) == ACTION_STATUS_CANCELLED); 95 | } 96 | 97 | TEST_CASE("wf::touch::drag_action_t") 98 | { 99 | drag_action_t drag{MOVE_DIRECTION_LEFT, 50}; 100 | drag.set_move_tolerance(5); 101 | 102 | gesture_state_t state; 103 | state.fingers[0] = finger_in_dir(-50, 0); 104 | state.fingers[1] = finger_in_dir(-50, 3); 105 | 106 | gesture_event_t ev; 107 | ev.type = EVENT_TYPE_MOTION; 108 | ev.time = 0; 109 | 110 | // check ok 111 | drag.reset(0); 112 | CHECK(drag.update_state(state, ev) == ACTION_STATUS_COMPLETED); 113 | 114 | // check distance not enough 115 | drag.reset(0); 116 | state.fingers[0] = finger_in_dir(-49, 0); 117 | CHECK(drag.update_state(state, ev) == ACTION_STATUS_RUNNING); 118 | 119 | // check exceeds tolerance 120 | state.fingers[1] = finger_in_dir(0, 6); 121 | drag.reset(0); 122 | CHECK(drag.update_state(state, ev) == ACTION_STATUS_CANCELLED); 123 | 124 | // check touch cancels 125 | ev.type = EVENT_TYPE_TOUCH_UP; 126 | state.fingers[1] = finger_in_dir(-50, 3); 127 | drag.reset(0); 128 | CHECK(drag.update_state(state, ev) == ACTION_STATUS_CANCELLED); 129 | } 130 | 131 | TEST_CASE("wf::touch::pinch_action_t") 132 | { 133 | pinch_action_t in{0.5}, out{2}; 134 | 135 | gesture_state_t state; 136 | state.fingers[0] = finger_2p(1, 0, 2, 1); 137 | state.fingers[1] = finger_2p(-1, -2, -3, -4); 138 | 139 | gesture_event_t ev; 140 | ev.time = 0; 141 | ev.type = EVENT_TYPE_MOTION; 142 | 143 | // ok 144 | out.reset(0); 145 | CHECK(out.update_state(state, ev) == ACTION_STATUS_COMPLETED); 146 | 147 | std::swap(state.fingers[0].origin, state.fingers[0].current); 148 | std::swap(state.fingers[1].origin, state.fingers[1].current); 149 | in.reset(0); 150 | CHECK(in.update_state(state, ev) == ACTION_STATUS_COMPLETED); 151 | 152 | // too much movement 153 | in.set_move_tolerance(1); 154 | in.reset(0); 155 | state.fingers[0].current += point_t{2, 0}; 156 | state.fingers[1].current += point_t{2, 0}; 157 | CHECK(in.update_state(state, ev) == ACTION_STATUS_CANCELLED); 158 | 159 | // touch cancels 160 | in.reset(0); 161 | state.fingers[0].current -= point_t{2, 0}; 162 | state.fingers[1].current -= point_t{2, 0}; 163 | ev.type = EVENT_TYPE_TOUCH_DOWN; 164 | CHECK(in.update_state(state, ev) == ACTION_STATUS_CANCELLED); 165 | } 166 | 167 | TEST_CASE("wf::touch::rotate_action_t") 168 | { 169 | gesture_state_t state; 170 | state.fingers[0] = finger_2p(0, 1, 1, 0); 171 | state.fingers[1] = finger_2p(1, 0, 0, -1); 172 | state.fingers[2] = finger_2p(0, -1, -1, 0); 173 | state.fingers[3] = finger_2p(-1, 0, 0, 1); 174 | CHECK(state.get_rotation_angle() == doctest::Approx(-M_PI / 2.0)); 175 | 176 | rotate_action_t rotate{-M_PI / 3.0}; 177 | gesture_event_t ev; 178 | ev.type = EVENT_TYPE_MOTION; 179 | CHECK(rotate.update_state(state, ev) == ACTION_STATUS_COMPLETED); 180 | 181 | // TODO: incomplete tests 182 | } 183 | -------------------------------------------------------------------------------- /test/basic_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | #include "shared.hpp" 4 | 5 | TEST_CASE("get_move_in_direction") 6 | { 7 | CHECK(finger_in_dir(1, 0).get_direction() == MOVE_DIRECTION_RIGHT); 8 | CHECK(finger_in_dir(-1, -1).get_direction() == lu); 9 | CHECK(finger_in_dir(1, 1).get_direction() == rd); 10 | CHECK(finger_in_dir(0, 0).get_direction() == 0); 11 | CHECK(finger_in_dir(-10, 1).get_direction() == MOVE_DIRECTION_LEFT); 12 | } 13 | 14 | TEST_CASE("get_drag_distance") 15 | { 16 | CHECK(finger_in_dir(0, 5).get_drag_distance(MOVE_DIRECTION_DOWN) 17 | == doctest::Approx(5)); 18 | CHECK(finger_in_dir(-1, -1).get_drag_distance(MOVE_DIRECTION_DOWN) 19 | == doctest::Approx(0)); 20 | } 21 | 22 | TEST_CASE("get_incorrect_drag_distance") 23 | { 24 | CHECK(finger_in_dir(-1, -1).get_incorrect_drag_distance(lu) == 25 | doctest::Approx(0)); 26 | CHECK(finger_in_dir(-1, -1).get_incorrect_drag_distance(ru) == 27 | doctest::Approx(std::sqrt(2))); 28 | CHECK(finger_in_dir(-1, -1).get_incorrect_drag_distance(ld) == 29 | doctest::Approx(std::sqrt(2))); 30 | CHECK(finger_in_dir(5, 5).get_incorrect_drag_distance(MOVE_DIRECTION_RIGHT) 31 | == doctest::Approx(5)); 32 | CHECK(finger_in_dir(4, 0).get_incorrect_drag_distance(MOVE_DIRECTION_LEFT) 33 | == doctest::Approx(4)); 34 | } 35 | 36 | TEST_CASE("get_pinch_scale") 37 | { 38 | gesture_state_t state; 39 | state.fingers[0] = finger_2p(1, 0, 2, 1); 40 | state.fingers[1] = finger_2p(-1, -2, -3, -4); 41 | CHECK(state.get_pinch_scale() > 2); 42 | 43 | std::swap(state.fingers[0].origin, state.fingers[0].current); 44 | std::swap(state.fingers[1].origin, state.fingers[1].current); 45 | CHECK(state.get_pinch_scale() < 0.5); 46 | 47 | state.fingers[0] = finger_2p(1, 1, 1, 1); 48 | state.fingers[1] = finger_2p(2, 2, 2, 2); 49 | CHECK(state.get_pinch_scale() == doctest::Approx(1)); 50 | } 51 | 52 | TEST_CASE("get_rotation_angle") 53 | { 54 | gesture_state_t state; 55 | state.fingers[0] = finger_2p(0, 1, 1, 0); 56 | state.fingers[1] = finger_2p(1, 0, 0, -1); 57 | state.fingers[2] = finger_2p(0, -1, -1, 0); 58 | state.fingers[3] = finger_2p(-1, 0, 0, 1); 59 | CHECK(state.get_rotation_angle() == doctest::Approx(-M_PI / 2.0)); 60 | 61 | // triangle (0, 0), (56, 15), (15, 56) is almost equilateral 62 | state.fingers.clear(); 63 | state.fingers[0] = finger_2p(0, 0, 56, 15); 64 | state.fingers[1] = finger_2p(56, 15, 15, 56); 65 | state.fingers[2] = finger_2p(15, 56, 0, 0); 66 | CHECK(state.get_rotation_angle() == 67 | doctest::Approx(2.0 * M_PI / 3.0).epsilon(0.05)); 68 | } 69 | 70 | TEST_CASE("finger_t") 71 | { 72 | CHECK(finger_in_dir(1, 1).delta() == point_t{1, 1}); 73 | } 74 | 75 | static void compare_point(const point_t& a, const point_t& b) 76 | { 77 | CHECK(a.x == doctest::Approx(b.x)); 78 | CHECK(a.y == doctest::Approx(b.y)); 79 | } 80 | 81 | static void compare_finger(const finger_t& a, const finger_t& b) 82 | { 83 | compare_point(a.origin, b.origin); 84 | compare_point(a.current, b.current); 85 | } 86 | 87 | TEST_CASE("gesture_state_t") 88 | { 89 | gesture_state_t state; 90 | state.fingers[0] = finger_in_dir(1, 2); 91 | state.fingers[1] = finger_in_dir(3, 4); 92 | state.fingers[2] = finger_in_dir(5, 6); 93 | compare_finger(state.get_center(), finger_in_dir(3, 4)); 94 | } 95 | 96 | TEST_CASE("gesture_state_t::update") 97 | { 98 | gesture_state_t state; 99 | 100 | gesture_event_t ev; 101 | ev.finger = 0; 102 | ev.pos = {4, 5}; 103 | ev.type = EVENT_TYPE_TOUCH_DOWN; 104 | state.update(ev); 105 | CHECK(state.fingers.size() == 1); 106 | compare_finger(state.fingers[0], finger_2p(4, 5, 4, 5)); 107 | 108 | ev.finger = 0; 109 | ev.pos = {6, 7}; 110 | ev.type = EVENT_TYPE_MOTION; 111 | state.update(ev); 112 | CHECK(state.fingers.size() == 1); 113 | compare_finger(state.fingers[0], finger_2p(4, 5, 6, 7)); 114 | 115 | ev.finger = 1; 116 | ev.pos = {7, -1}; 117 | ev.type = EVENT_TYPE_TOUCH_DOWN; 118 | state.update(ev); 119 | CHECK(state.fingers.size() == 2); 120 | compare_finger(state.fingers[0], finger_2p(4, 5, 6, 7)); 121 | compare_finger(state.fingers[1], finger_2p(7, -1, 7, -1)); 122 | 123 | ev.type = EVENT_TYPE_TOUCH_UP; 124 | ev.finger = 0; 125 | state.update(ev); 126 | CHECK(state.fingers.size() == 1); 127 | compare_finger(state.fingers[1], finger_2p(7, -1, 7, -1)); 128 | } 129 | 130 | TEST_CASE("gesture_state_t::reset_origin") 131 | { 132 | gesture_state_t state; 133 | state.fingers[0] = finger_in_dir(6, 7); 134 | state.reset_origin(); 135 | CHECK(state.fingers.size() == 1); 136 | compare_finger(state.fingers[0], finger_2p(6, 7, 6, 7)); 137 | } 138 | 139 | TEST_CASE("touch_target_t") 140 | { 141 | touch_target_t target{-1, 1, 2, 2}; 142 | CHECK(target.contains({0, 2})); 143 | CHECK(target.contains({-1, 1})); 144 | CHECK(!target.contains({1, 3})); 145 | CHECK(!target.contains({0, 5})); 146 | } 147 | -------------------------------------------------------------------------------- /test/gesture_test.cpp: -------------------------------------------------------------------------------- 1 | #define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN 2 | #include 3 | #include 4 | 5 | using namespace wf::touch; 6 | 7 | class fake_timer_t : public timer_interface_t 8 | { 9 | public: 10 | std::vector requests; 11 | std::function last_cb; 12 | 13 | void set_timeout(uint32_t req, std::function cb) override 14 | { 15 | requests.push_back(req); 16 | last_cb = cb; 17 | } 18 | 19 | void reset() override 20 | { 21 | requests.push_back(-1); 22 | } 23 | }; 24 | 25 | TEST_CASE("wf::touch::gesture_t") 26 | { 27 | int completed = 0; 28 | int cancelled = 0; 29 | gesture_callback_t callback1 = [&] () 30 | { 31 | ++completed; 32 | }; 33 | 34 | gesture_callback_t callback2 = [&] () 35 | { 36 | ++cancelled; 37 | }; 38 | 39 | auto _timer = std::make_unique(); 40 | auto timer_ptr = _timer.get(); 41 | 42 | #define REQUIRE_TIMERS(...) CHECK(timer_ptr->requests == std::vector{__VA_ARGS__}); 43 | 44 | SUBCASE("Hold gesture") 45 | { 46 | gesture_t hold = gesture_builder_t() 47 | .action(touch_action_t(2, true).set_duration(100)) 48 | .action(hold_action_t(200)) 49 | .on_completed(callback1) 50 | .on_cancelled(callback2) 51 | .build(); 52 | 53 | hold.set_timer(std::move(_timer)); 54 | 55 | hold.reset(0); 56 | hold.update_state( 57 | gesture_event_t{ .type = EVENT_TYPE_TOUCH_DOWN, .time = 0, .finger = 0, .pos = {0, 0}}); 58 | 59 | REQUIRE_TIMERS(100); 60 | 61 | SUBCASE("Timeout") 62 | { 63 | timer_ptr->last_cb(); 64 | CHECK(hold.get_status() == ACTION_STATUS_CANCELLED); 65 | } 66 | 67 | SUBCASE("OK") 68 | { 69 | hold.update_state( 70 | gesture_event_t{ .type = EVENT_TYPE_TOUCH_DOWN, .time = 10, .finger = 1, .pos = {0, 0}}); 71 | REQUIRE_TIMERS(100, -1, 200); 72 | timer_ptr->last_cb(); 73 | CHECK(hold.get_status() == ACTION_STATUS_COMPLETED); 74 | REQUIRE_TIMERS(100, -1, 200, -1); 75 | } 76 | } 77 | 78 | SUBCASE("double-tap gesture") 79 | { 80 | gesture_t double_tap = gesture_builder_t() 81 | .action(touch_action_t(1, true).set_duration(100)) 82 | .action(touch_action_t(1, false).set_duration(100)) 83 | .action(touch_action_t(1, true).set_duration(100)) 84 | .action(touch_action_t(1, false)) 85 | .on_completed(callback1) 86 | .on_cancelled(callback2) 87 | .build(); 88 | 89 | double_tap.set_timer(std::move(_timer)); 90 | 91 | double_tap.reset(0); 92 | double_tap.update_state({.type = EVENT_TYPE_TOUCH_DOWN, .time = 0, .finger = 0, .pos = {0, 0}}); 93 | double_tap.update_state({.type = EVENT_TYPE_TOUCH_UP, .time = 20, .finger = 0, .pos = {0, 0}}); 94 | CHECK(double_tap.get_status() == ACTION_STATUS_RUNNING); 95 | 96 | SUBCASE("Success") 97 | { 98 | double_tap.reset(80); 99 | double_tap.update_state({.type = EVENT_TYPE_TOUCH_DOWN, .time = 80, .finger = 0, .pos = {0, 0}}); 100 | double_tap.update_state({.type = EVENT_TYPE_TOUCH_UP, .time = 90, .finger = 0, .pos = {0, 0}}); 101 | CHECK(completed == 1); 102 | CHECK(cancelled == 0); 103 | } 104 | 105 | SUBCASE("Timeout") 106 | { 107 | REQUIRE_TIMERS(100, -1, 100, -1, 100); 108 | 109 | timer_ptr->last_cb(); 110 | CHECK(double_tap.get_status() == ACTION_STATUS_CANCELLED); 111 | CHECK(completed == 0); 112 | CHECK(cancelled == 1); 113 | REQUIRE_TIMERS(100, -1, 100, -1, 100, -1); 114 | 115 | double_tap.reset(150); 116 | CHECK(double_tap.get_status() == ACTION_STATUS_RUNNING); 117 | } 118 | } 119 | 120 | SUBCASE("swipe") 121 | { 122 | gesture_t swipe = gesture_builder_t() 123 | .action(touch_action_t(1, true)) 124 | .action(hold_action_t(5)) 125 | .action(drag_action_t(MOVE_DIRECTION_LEFT, 10)) 126 | .action(hold_action_t(5)) 127 | .action(drag_action_t(MOVE_DIRECTION_RIGHT, 10)) 128 | .on_completed(callback1) 129 | .on_cancelled(callback2) 130 | .build(); 131 | 132 | swipe.set_timer(std::move(_timer)); 133 | swipe.reset(0); 134 | gesture_event_t touch_down; 135 | touch_down.finger = 0; 136 | touch_down.pos = {0, 0}; 137 | touch_down.type = EVENT_TYPE_TOUCH_DOWN; 138 | touch_down.time = 0; 139 | swipe.update_state(touch_down); 140 | CHECK(swipe.get_progress() >= 0.2); 141 | 142 | SUBCASE("complete") 143 | { 144 | timer_ptr->last_cb(); 145 | gesture_event_t motion_left; 146 | motion_left.finger = 0; 147 | motion_left.pos = {-10, 0}; 148 | motion_left.time = 10; 149 | motion_left.type = EVENT_TYPE_MOTION; 150 | swipe.update_state(motion_left); 151 | 152 | timer_ptr->last_cb(); 153 | 154 | CHECK(cancelled == 0); 155 | CHECK(completed == 0); 156 | CHECK(swipe.get_progress() >= 0.6); 157 | 158 | gesture_event_t motion_right = motion_left; 159 | motion_right.pos = {0, 0}; 160 | motion_right.time = 20; 161 | swipe.update_state(motion_right); 162 | CHECK(cancelled == 0); 163 | CHECK(completed == 1); 164 | 165 | SUBCASE("restart") 166 | { 167 | swipe.reset(0); 168 | CHECK(swipe.get_progress() == 0.0); 169 | swipe.update_state(touch_down); 170 | timer_ptr->last_cb(); 171 | swipe.update_state(motion_left); 172 | timer_ptr->last_cb(); 173 | swipe.update_state(motion_right); 174 | CHECK(cancelled == 0); 175 | CHECK(completed == 2); 176 | } 177 | } 178 | 179 | SUBCASE("cancelled") 180 | { 181 | touch_down.finger = 1; 182 | swipe.update_state(touch_down); 183 | CHECK(cancelled == 1); 184 | CHECK(completed == 0); 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /test/meson.build: -------------------------------------------------------------------------------- 1 | basic_test = executable( 2 | 'basic_test', 3 | 'basic_test.cpp', 4 | dependencies: [wftouch, doctest], 5 | install: false) 6 | test('Basic test', basic_test) 7 | 8 | action_test = executable( 9 | 'action_test', 10 | 'action_test.cpp', 11 | dependencies: [wftouch, doctest], 12 | install: false) 13 | test('Action test', action_test) 14 | 15 | gesture_test = executable( 16 | 'gesture_test', 17 | 'gesture_test.cpp', 18 | dependencies: [wftouch, doctest], 19 | install: false) 20 | test('Gesture test', gesture_test) 21 | -------------------------------------------------------------------------------- /test/shared.hpp: -------------------------------------------------------------------------------- 1 | #define _USE_MATH_DEFINES 2 | #include 3 | 4 | #include 5 | using namespace wf::touch; 6 | 7 | static finger_t finger_in_dir(double x, double y) 8 | { 9 | return finger_t { 10 | .origin = {0, 0}, 11 | .current = {x, y} 12 | }; 13 | } 14 | 15 | const uint32_t lu = MOVE_DIRECTION_LEFT | MOVE_DIRECTION_UP; 16 | const uint32_t ld = MOVE_DIRECTION_LEFT | MOVE_DIRECTION_DOWN; 17 | const uint32_t rd = MOVE_DIRECTION_RIGHT | MOVE_DIRECTION_DOWN; 18 | const uint32_t ru = MOVE_DIRECTION_RIGHT | MOVE_DIRECTION_UP; 19 | 20 | static finger_t finger_2p(double x, double y, double a, double b) 21 | { 22 | return finger_t { 23 | .origin = {x, y}, 24 | .current = {a, b} 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /wayfire/touch/touch.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /** 4 | * Touchscreen gesture library, designed for use in Wayfire (and elsewhere). 5 | * Goal is to process touch events and detect various configurable gestures. 6 | * 7 | * High-level design: 8 | * A gesture consists of one or more consecutive actions. 9 | * 10 | * An action is usually a simple part of the gesture which can be processed 11 | * separately, for ex. touch down with 3 fingers, swipe in a direction, etc. 12 | * 13 | * When processing events, the gesture starts with its first action. Once it is 14 | * completed, the processing continues with the next action, and so on, until 15 | * either all actions are completed or an action cancels the gesture. 16 | */ 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | namespace wf 25 | { 26 | namespace touch 27 | { 28 | using point_t = glm::dvec2; 29 | 30 | 31 | /** 32 | * Movement direction. 33 | */ 34 | enum move_direction_t 35 | { 36 | MOVE_DIRECTION_LEFT = (1 << 0), 37 | MOVE_DIRECTION_RIGHT = (1 << 1), 38 | MOVE_DIRECTION_UP = (1 << 2), 39 | MOVE_DIRECTION_DOWN = (1 << 3), 40 | }; 41 | 42 | struct finger_t 43 | { 44 | point_t origin; 45 | point_t current; 46 | 47 | /** Get movement vector */ 48 | point_t delta() const; 49 | 50 | /** Find direction of movement, a bitmask of move_direction_t */ 51 | uint32_t get_direction() const; 52 | 53 | /** Find drag distance in the given direction */ 54 | double get_drag_distance(uint32_t direction) const; 55 | 56 | /** Find drag distance in opposite and perpendicular directions */ 57 | double get_incorrect_drag_distance(uint32_t direction) const; 58 | }; 59 | 60 | enum gesture_event_type_t 61 | { 62 | /** Finger touched down the screen */ 63 | EVENT_TYPE_TOUCH_DOWN, 64 | /** Finger was lifted off the screen */ 65 | EVENT_TYPE_TOUCH_UP, 66 | /** Finger moved across the screen */ 67 | EVENT_TYPE_MOTION, 68 | /** Timeout since action start */ 69 | EVENT_TYPE_TIMEOUT, 70 | }; 71 | 72 | /** 73 | * Represents a single update on the touch state. 74 | */ 75 | struct gesture_event_t 76 | { 77 | /** type of the event */ 78 | gesture_event_type_t type; 79 | /** timestamp of the event in milliseconds */ 80 | uint32_t time{}; 81 | /** finger id which the event is about */ 82 | int32_t finger{}; 83 | 84 | /** coordinates of the finger */ 85 | point_t pos{}; 86 | }; 87 | 88 | /** 89 | * Contains all fingers. 90 | */ 91 | struct gesture_state_t 92 | { 93 | public: 94 | // finger_id -> finger_t 95 | std::map fingers; 96 | 97 | /** Update fingers based on the event */ 98 | void update(const gesture_event_t& event); 99 | 100 | /** Reset finger origin to current positions */ 101 | void reset_origin(); 102 | 103 | /** Find the center points of the fingers. */ 104 | finger_t get_center() const; 105 | 106 | /** Get the pinch scale of current touch points. */ 107 | double get_pinch_scale() const; 108 | 109 | /** 110 | * Get the rotation angle in radians of current touch points. 111 | * NB: Works only for rotation < 180 degrees. 112 | */ 113 | double get_rotation_angle() const; 114 | }; 115 | 116 | /** 117 | * Represents the status of an action after it is updated 118 | */ 119 | enum action_status_t 120 | { 121 | /** Action is done after this event. */ 122 | ACTION_STATUS_COMPLETED, 123 | /** Action is still running after this event. */ 124 | ACTION_STATUS_RUNNING, 125 | /** The whole gesture should be cancelled. */ 126 | ACTION_STATUS_CANCELLED, 127 | }; 128 | 129 | /** 130 | * Represents a part of the gesture. 131 | */ 132 | class gesture_action_t 133 | { 134 | public: 135 | /** 136 | * Set the duration of the action in milliseconds. 137 | * 138 | * After the duration times out, the action will receive 139 | * 140 | * 141 | * This is the maximal time needed for this action to be happening to 142 | * consider it complete. 143 | * 144 | * @return this 145 | */ 146 | gesture_action_t& set_duration(uint32_t duration); 147 | 148 | /** @return The duration of the gesture action. */ 149 | std::optional get_duration() const; 150 | 151 | /** 152 | * Update the action's state according to the new state. 153 | * 154 | * NOTE: The actual implementation should update the @start_time field. 155 | * 156 | * @param state The gesture state since the last reset of the gesture. 157 | * @param event The event causing this update. 158 | * @return The new action status. 159 | */ 160 | virtual action_status_t update_state(const gesture_state_t& state, 161 | const gesture_event_t& event) = 0; 162 | 163 | /** 164 | * Reset the action. 165 | * Called whenever the action is started again. 166 | */ 167 | virtual void reset(uint32_t time); 168 | 169 | virtual ~gesture_action_t() {} 170 | 171 | protected: 172 | gesture_action_t() {} 173 | 174 | /** Time of the first event. */ 175 | int64_t start_time; 176 | 177 | private: 178 | std::optional duration; // maximal duration 179 | }; 180 | 181 | #define WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(x) \ 182 | x& set_move_tolerance(double tolerance) \ 183 | { \ 184 | this->move_tolerance = tolerance; \ 185 | return *this; \ 186 | } \ 187 | x& set_duration(uint32_t duration) \ 188 | { \ 189 | gesture_action_t::set_duration(duration); \ 190 | return *this; \ 191 | } 192 | 193 | /** 194 | * Represents a target area where the touch event takes place. 195 | */ 196 | struct touch_target_t 197 | { 198 | double x; 199 | double y; 200 | double width; 201 | double height; 202 | 203 | bool contains(const point_t& point) const; 204 | }; 205 | 206 | /** 207 | * Represents the action of touching down with several fingers. 208 | */ 209 | class touch_action_t : public gesture_action_t 210 | { 211 | public: 212 | /** 213 | * Create a new touch down or up action. 214 | * 215 | * @param cnt_fingers The number of fingers that need to be touched down 216 | * or released to consider the action completed. 217 | * @param touch_down Whether the action is touch down or touch up. 218 | */ 219 | touch_action_t(int cnt_fingers, bool touch_down); 220 | WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(touch_action_t); 221 | 222 | /** 223 | * Set the target area of this gesture. 224 | * 225 | * @return this 226 | */ 227 | touch_action_t& set_target(const touch_target_t& target); 228 | 229 | /** 230 | * Mark the action as completed iff state has the right amount of fingers 231 | * and if the event is a touch down. 232 | */ 233 | action_status_t update_state(const gesture_state_t& state, 234 | const gesture_event_t& event) override; 235 | 236 | void reset(uint32_t time) override; 237 | 238 | protected: 239 | /** @return True if the fingers have moved too much. */ 240 | bool exceeds_tolerance(const gesture_state_t& state); 241 | 242 | private: 243 | int cnt_fingers; 244 | int cnt_touch_events; 245 | gesture_event_type_t type; 246 | uint32_t move_tolerance = 1e9; 247 | 248 | touch_target_t target; 249 | }; 250 | 251 | /** 252 | * Represents the action of holding the fingers still for a certain amount 253 | * of time. 254 | */ 255 | class hold_action_t : public gesture_action_t 256 | { 257 | public: 258 | /** 259 | * Create a new hold action. 260 | * 261 | * @param threshold The time is milliseconds needed to consider the gesture 262 | * complete. 263 | */ 264 | hold_action_t(int32_t threshold); 265 | WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(hold_action_t); 266 | 267 | /** 268 | * The action is already completed iff no fingers have been added or 269 | * released and the given amount of time has passed without much movement. 270 | */ 271 | action_status_t update_state(const gesture_state_t& state, 272 | const gesture_event_t& event) override; 273 | 274 | protected: 275 | /** @return True if the fingers have moved too much. */ 276 | bool exceeds_tolerance(const gesture_state_t& state); 277 | 278 | private: 279 | uint32_t move_tolerance = 1e9; 280 | }; 281 | 282 | /** 283 | * Represents the action of dragging the fingers in a particular direction 284 | * over a particular distance. 285 | */ 286 | class drag_action_t : public gesture_action_t 287 | { 288 | public: 289 | /** 290 | * Create a new drag action. 291 | * 292 | * @param direction The direction of the drag action. 293 | * @param threshold The distance that needs to be covered. 294 | */ 295 | drag_action_t(uint32_t direction, double threshold); 296 | WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(drag_action_t); 297 | 298 | /** 299 | * The action is already completed iff no fingers have been added or 300 | * released and the given amount of time has passed without much movement. 301 | */ 302 | action_status_t update_state(const gesture_state_t& state, 303 | const gesture_event_t& event) override; 304 | 305 | protected: 306 | /** 307 | * @return True if any finger has moved more than the threshold in an 308 | * incorrect direction. 309 | */ 310 | bool exceeds_tolerance(const gesture_state_t& state); 311 | 312 | private: 313 | double threshold; 314 | uint32_t direction; 315 | uint32_t move_tolerance = 1e9; 316 | }; 317 | 318 | /** 319 | * Represents a pinch action. 320 | */ 321 | class pinch_action_t : public gesture_action_t 322 | { 323 | public: 324 | /** 325 | * Create a new pinch action. 326 | * 327 | * @param threshold The threshold to be exceeded. 328 | * If threshold is less/more than 1, then the action is complete when 329 | * the actual pinch scale is respectively less/more than threshold. 330 | */ 331 | pinch_action_t(double threshold); 332 | WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(pinch_action_t); 333 | 334 | /** 335 | * The action is already completed iff no fingers have been added or 336 | * released and the pinch threshold has been reached without much movement. 337 | */ 338 | action_status_t update_state(const gesture_state_t& state, 339 | const gesture_event_t& event) override; 340 | 341 | protected: 342 | /** 343 | * @return True if gesture center has moved more than tolerance. 344 | */ 345 | bool exceeds_tolerance(const gesture_state_t& state); 346 | 347 | private: 348 | double threshold; 349 | uint32_t move_tolerance = 1e9; 350 | }; 351 | 352 | /** 353 | * Represents a rotate action. 354 | */ 355 | class rotate_action_t : public gesture_action_t 356 | { 357 | public: 358 | /** 359 | * Create a new rotate action. 360 | * 361 | * @param threshold The threshold to be exceeded. 362 | * If threshold is less/more than 0, then the action is complete when 363 | * the actual rotation angle is respectively less/more than threshold. 364 | */ 365 | rotate_action_t(double threshold); 366 | WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(rotate_action_t); 367 | 368 | /** 369 | * The action is already completed iff no fingers have been added or 370 | * released and the rotation threshold has been reached without much movement. 371 | */ 372 | action_status_t update_state(const gesture_state_t& state, 373 | const gesture_event_t& event) override; 374 | 375 | protected: 376 | /** 377 | * @return True if gesture center has moved more than tolerance. 378 | */ 379 | bool exceeds_tolerance(const gesture_state_t& state); 380 | 381 | private: 382 | double threshold; 383 | uint32_t move_tolerance = 1e9; 384 | }; 385 | 386 | using gesture_callback_t = std::function; 387 | 388 | class timer_interface_t 389 | { 390 | public: 391 | virtual void set_timeout(uint32_t msec, std::function handler) = 0; 392 | virtual void reset() = 0; 393 | virtual ~timer_interface_t() = default; 394 | }; 395 | 396 | /** 397 | * Represents a series of actions forming a gesture together. 398 | */ 399 | class gesture_t 400 | { 401 | public: 402 | /** 403 | * Create a new gesture consisting of the given actions. 404 | * 405 | * @param actions The actions the gesture consists of. 406 | * @param completed The callback to execute each time the gesture is 407 | * completed. 408 | * @param cancelled The callback to execute each time the gesture is 409 | * cancelled. 410 | */ 411 | gesture_t(std::vector> actions = {}, 412 | gesture_callback_t completed = [](){}, gesture_callback_t cancelled = [](){}); 413 | 414 | gesture_t(gesture_t&& other); 415 | gesture_t& operator=(gesture_t&& other); 416 | 417 | ~gesture_t(); 418 | 419 | /** @return What percentage of the actions are complete. */ 420 | double get_progress() const; 421 | 422 | /** 423 | * Update the gesture state. 424 | * 425 | * @param event The next event. 426 | */ 427 | void update_state(const gesture_event_t& event); 428 | 429 | /** 430 | * Get the current state of the gesture. 431 | */ 432 | action_status_t get_status() const; 433 | 434 | /** 435 | * Reset the gesture state. 436 | * 437 | * @param time The time of the event causing the start of gesture 438 | * recognition, this is typically the first touch event. 439 | */ 440 | void reset(uint32_t time); 441 | 442 | /** 443 | * Set the timer to use for the gesture. 444 | * This needs to be called before using the gesture. 445 | * 446 | * In wayfire, this is usually set by core. 447 | */ 448 | void set_timer(std::unique_ptr timer); 449 | 450 | private: 451 | class impl; 452 | std::unique_ptr priv; 453 | }; 454 | 455 | /** 456 | * A helper class to facilitate gesture construction. 457 | */ 458 | class gesture_builder_t 459 | { 460 | public: 461 | gesture_builder_t(); 462 | 463 | template 464 | gesture_builder_t& action(const ActionType& action) 465 | { 466 | actions.push_back(std::make_unique(action)); 467 | return *this; 468 | } 469 | 470 | gesture_builder_t& on_completed(gesture_callback_t callback); 471 | gesture_builder_t& on_cancelled(gesture_callback_t callback); 472 | gesture_t build(); 473 | 474 | private: 475 | gesture_callback_t _on_completed = [](){}; 476 | gesture_callback_t _on_cancelled = [](){}; 477 | std::vector> actions; 478 | }; 479 | } 480 | } 481 | --------------------------------------------------------------------------------