pax_global_header00006660000000000000000000000064140251356470014521gustar00rootroot0000000000000052 comment=8974eb0f6a65464b63dd03b842795cb441fb6403 wayfire-0.0~git20210319.8974eb0/000077500000000000000000000000001402513564700155535ustar00rootroot00000000000000wayfire-0.0~git20210319.8974eb0/LICENSE000066400000000000000000000020501402513564700165550ustar00rootroot00000000000000MIT License Copyright (c) 2020 Wayfire Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. wayfire-0.0~git20210319.8974eb0/README.md000066400000000000000000000003461402513564700170350ustar00rootroot00000000000000# wf-touch Touchscreen gesture library # Acknowledgements The library's design has been heavily inspired by https://github.com/grahnen/libtouch, which has also been used as a reference for some implementation details at times. wayfire-0.0~git20210319.8974eb0/meson.build000066400000000000000000000011651402513564700177200ustar00rootroot00000000000000project('wf-touch', ['cpp'], version : '0.0', meson_version: '>=0.47.0', default_options : ['cpp_std=c++17']) glm = dependency('glm') wf_touch_inc_dirs = include_directories('.') install_headers([ 'wayfire/touch/touch.hpp'], subdir: 'wayfire/touch') wftouch_lib = static_library('wftouch', ['src/touch.cpp', 'src/actions.cpp', 'src/math.cpp'], dependencies: glm, install: true) wftouch = declare_dependency(link_with: wftouch_lib, include_directories: wf_touch_inc_dirs, dependencies: glm) doctest = dependency('doctest', required: get_option('tests')) if doctest.found() subdir('test') endif wayfire-0.0~git20210319.8974eb0/meson_options.txt000066400000000000000000000001221402513564700212030ustar00rootroot00000000000000option('tests', type: 'feature', value: 'auto', description: 'Enable unit tests') wayfire-0.0~git20210319.8974eb0/src/000077500000000000000000000000001402513564700163425ustar00rootroot00000000000000wayfire-0.0~git20210319.8974eb0/src/actions.cpp000066400000000000000000000131671402513564700205160ustar00rootroot00000000000000#include #include #include #define _ << " " << #define debug(x) #x << " = " << (x) using namespace wf::touch; /* -------------------------- Touch action ---------------------------------- */ wf::touch::touch_action_t::touch_action_t(int cnt_fingers, bool touch_down) { this->cnt_fingers = cnt_fingers; this->type = touch_down ? EVENT_TYPE_TOUCH_DOWN : EVENT_TYPE_TOUCH_UP; this->target.x = -1e9; this->target.y = -1e9; this->target.width = 2e9; this->target.height = 2e9; } void wf::touch::touch_action_t::set_target(const touch_target_t& target) { this->target = target; } static double find_max_delta(const gesture_state_t& state) { double max_length = 0; for (auto& f : state.fingers) { max_length = std::max(max_length, glm::length(f.second.delta())); } return max_length; } bool wf::touch::touch_action_t::exceeds_tolerance(const gesture_state_t& state) { return find_max_delta(state) > this->get_move_tolerance(); } void wf::touch::touch_action_t::reset(uint32_t time) { gesture_action_t::reset(time); this->released_fingers = 0; } action_status_t wf::touch::touch_action_t::update_state( const gesture_state_t& state, const gesture_event_t& event) { // Allow motion events because of tolerance if (this->type != event.type && event.type != EVENT_TYPE_MOTION) { return ACTION_STATUS_CANCELLED; } for (auto& f : state.fingers) { point_t relevant_point = (this->type == EVENT_TYPE_TOUCH_UP ? f.second.current : f.second.origin); if (!this->target.contains(relevant_point)) { return ACTION_STATUS_CANCELLED; } } if (event.type == EVENT_TYPE_MOTION) { return calculate_next_status(state, event, true); } if (this->type == EVENT_TYPE_TOUCH_DOWN) { if (this->cnt_fingers < (int)state.fingers.size()) { return ACTION_STATUS_CANCELLED; } return calculate_next_status(state, event, this->cnt_fingers > (int)state.fingers.size()); } else { ++this->released_fingers; return calculate_next_status(state, event, this->released_fingers < this->cnt_fingers); } } /*- -------------------------- Hold action ---------------------------------- */ wf::touch::hold_action_t::hold_action_t(int32_t threshold) { this->threshold = threshold; } action_status_t wf::touch::hold_action_t::update_state(const gesture_state_t& state, const gesture_event_t& event) { bool action_done = ((event.time - this->start_time) >= this->threshold); if (!action_done && event.type != EVENT_TYPE_MOTION) { return ACTION_STATUS_CANCELLED; } if (action_done) { return ACTION_STATUS_ALREADY_COMPLETED; } return calculate_next_status(state, event, true); } bool wf::touch::hold_action_t::exceeds_tolerance(const gesture_state_t& state) { return find_max_delta(state) > this->get_move_tolerance(); } /*- -------------------------- Drag action ---------------------------------- */ wf::touch::drag_action_t::drag_action_t(uint32_t direction, double threshold) { this->direction = direction; this->threshold = threshold; } action_status_t wf::touch::drag_action_t::update_state(const gesture_state_t& state, const gesture_event_t& event) { if (event.type != EVENT_TYPE_MOTION) { return ACTION_STATUS_CANCELLED; } const double dragged = state.get_center().get_drag_distance(this->direction); return calculate_next_status(state, event, dragged < this->threshold); } bool wf::touch::drag_action_t::exceeds_tolerance(const gesture_state_t& state) { for (auto& f : state.fingers) { if (f.second.get_incorrect_drag_distance(this->direction) > this->get_move_tolerance()) { return true; } } return false; } /*- -------------------------- Pinch action ---------------------------------- */ wf::touch::pinch_action_t::pinch_action_t(double threshold) { this->threshold = threshold; } action_status_t wf::touch::pinch_action_t::update_state(const gesture_state_t& state, const gesture_event_t& event) { if (event.type != EVENT_TYPE_MOTION) { return ACTION_STATUS_CANCELLED; } bool running = true; const double current_scale = state.get_pinch_scale(); if (((this->threshold < 1.0) && (current_scale <= threshold)) || ((this->threshold > 1.0) && (current_scale >= threshold))) { running = false; } return calculate_next_status(state, event, running); } bool wf::touch::pinch_action_t::exceeds_tolerance(const gesture_state_t& state) { return glm::length(state.get_center().delta()) > this->get_move_tolerance(); } /*- -------------------------- Rotate action ---------------------------------- */ wf::touch::rotate_action_t::rotate_action_t(double threshold) { this->threshold = threshold; } action_status_t wf::touch::rotate_action_t::update_state(const gesture_state_t& state, const gesture_event_t& event) { if (event.type != EVENT_TYPE_MOTION) { return ACTION_STATUS_CANCELLED; } bool running = true; const double current_scale = state.get_rotation_angle(); if (((this->threshold < 0.0) && (current_scale <= threshold)) || ((this->threshold > 0.0) && (current_scale >= threshold))) { running = false; } return calculate_next_status(state, event, running); } bool wf::touch::rotate_action_t::exceeds_tolerance(const gesture_state_t& state) { return glm::length(state.get_center().delta()) > this->get_move_tolerance(); } wayfire-0.0~git20210319.8974eb0/src/math.cpp000066400000000000000000000067221402513564700200060ustar00rootroot00000000000000#define GLM_ENABLE_EXPERIMENTAL // for glm::orientedAngle #include #include #include #include #include #define _ << " " << #define debug(x) #x << " = " << (x) static constexpr double DIRECTION_TAN_THRESHOLD = 1.0 / 3.0; using namespace wf::touch; uint32_t wf::touch::finger_t::get_direction() const { double to_left = this->get_drag_distance(MOVE_DIRECTION_LEFT); double to_right = this->get_drag_distance(MOVE_DIRECTION_RIGHT); double to_up = this->get_drag_distance(MOVE_DIRECTION_UP); double to_down = this->get_drag_distance(MOVE_DIRECTION_DOWN); double horizontal = std::max(to_left, to_right); double vertical = std::max(to_up, to_down); uint32_t result = 0; if (to_left > 0 && to_left / vertical >= DIRECTION_TAN_THRESHOLD) { result |= MOVE_DIRECTION_LEFT; } else if (to_right > 0 && to_right / vertical >= DIRECTION_TAN_THRESHOLD) { result |= MOVE_DIRECTION_RIGHT; } if (to_up > 0 && to_up / horizontal >= DIRECTION_TAN_THRESHOLD) { result |= MOVE_DIRECTION_UP; } else if (to_down > 0 && to_down / horizontal >= DIRECTION_TAN_THRESHOLD) { result |= MOVE_DIRECTION_DOWN; } return result; } /** Get normal vector in direction */ static point_t get_dir_nv(uint32_t direction) { assert((direction != 0) && ((direction & 0b1111) == direction)); point_t dir = {0, 0}; if (direction & MOVE_DIRECTION_LEFT) { dir.x = -1; } else if (direction & MOVE_DIRECTION_RIGHT) { dir.x = 1; } if (direction & MOVE_DIRECTION_UP) { dir.y = -1; } else if (direction & MOVE_DIRECTION_DOWN) { dir.y = 1; } return dir; } double wf::touch::finger_t::get_drag_distance(uint32_t direction) const { const auto normal = get_dir_nv(direction); const auto delta = this->delta(); /* grahm-schmidt */ const double amount_alongside_dir = glm::dot(delta, normal) / glm::dot(normal, normal); if (amount_alongside_dir >= 0) { return glm::length(amount_alongside_dir * normal); } return 0; } double wf::touch::finger_t::get_incorrect_drag_distance(uint32_t direction) const { const auto normal = get_dir_nv(direction); const auto delta = this->delta(); /* grahm-schmidt */ double amount_alongside_dir = glm::dot(delta, normal) / glm::dot(normal, normal); if (amount_alongside_dir < 0) { /* Drag in opposite direction */ return glm::length(delta); } const auto residual = delta - normal * amount_alongside_dir; return glm::length(residual); } double wf::touch::gesture_state_t::get_pinch_scale() const { auto center = get_center(); double old_dist = 0; double new_dist = 0; for (const auto& f : fingers) { old_dist += glm::length(f.second.origin - center.origin); new_dist += glm::length(f.second.current - center.current); } old_dist /= fingers.size(); new_dist /= fingers.size(); return new_dist / old_dist; } double wf::touch::gesture_state_t::get_rotation_angle() const { auto center = get_center(); double angle_sum = 0; for (const auto& f : fingers) { auto v1 = glm::normalize(f.second.origin - center.origin); auto v2 = glm::normalize(f.second.current - center.current); angle_sum += glm::orientedAngle(v1, v2); } angle_sum /= fingers.size(); return angle_sum; } wayfire-0.0~git20210319.8974eb0/src/touch.cpp000066400000000000000000000121221402513564700201660ustar00rootroot00000000000000#include #include #define _ << " " << #define debug(x) #x << " = " << (x) using namespace wf::touch; point_t wf::touch::finger_t::delta() const { return this->current - this->origin; } finger_t wf::touch::gesture_state_t::get_center() const { finger_t center; center.origin = {0, 0}; center.current = {0, 0}; for (auto& f : this->fingers) { center.origin += f.second.origin; center.current += f.second.current; } center.origin /= this->fingers.size(); center.current /= this->fingers.size(); return center; } void wf::touch::gesture_state_t::update(const gesture_event_t& event) { switch (event.type) { case EVENT_TYPE_TOUCH_DOWN: fingers[event.finger].origin = event.pos; // fallthrough case EVENT_TYPE_MOTION: fingers[event.finger].current = event.pos; break; case EVENT_TYPE_TOUCH_UP: fingers.erase(event.finger); break; } } void wf::touch::gesture_state_t::reset_origin() { for (auto& f : fingers) { f.second.origin = f.second.current; } } void wf::touch::gesture_action_t::set_move_tolerance(double tolerance) { this->tolerance = tolerance; } double wf::touch::gesture_action_t::get_move_tolerance() const { return this->tolerance; } void wf::touch::gesture_action_t::set_duration(uint32_t duration) { this->duration = duration; } uint32_t wf::touch::gesture_action_t::get_duration() const { return this->duration; } action_status_t wf::touch::gesture_action_t::calculate_next_status( const gesture_state_t& state, const gesture_event_t& last_event, bool running) { uint32_t elapsed = last_event.time - this->start_time; if ((elapsed > this->get_duration()) || exceeds_tolerance(state)) { return ACTION_STATUS_CANCELLED; } return running ? ACTION_STATUS_RUNNING : ACTION_STATUS_COMPLETED; } bool wf::touch::gesture_action_t::exceeds_tolerance(const gesture_state_t&) { return false; } void wf::touch::gesture_action_t::reset(uint32_t time) { this->start_time = time; } bool wf::touch::touch_target_t::contains(const point_t& pt) const { return x <= pt.x && pt.x < x + width && y <= pt.y && pt.y < y + height; } class wf::touch::gesture_t::impl { public: gesture_callback_t completed; gesture_callback_t cancelled; std::vector> actions; size_t current_action = 0; action_status_t status = ACTION_STATUS_CANCELLED; gesture_state_t finger_state; }; wf::touch::gesture_t::gesture_t(std::vector> actions, gesture_callback_t completed, gesture_callback_t cancelled) { assert(!actions.empty()); this->priv = std::make_unique(); priv->actions = std::move(actions); priv->completed = completed; priv->cancelled = cancelled; } wf::touch::gesture_t::~gesture_t() = default; double wf::touch::gesture_t::get_progress() const { if (priv->status == ACTION_STATUS_CANCELLED) { return 0.0; } return 1.0 * priv->current_action / priv->actions.size(); } void wf::touch::gesture_t::update_state(const gesture_event_t& event) { if (priv->status != ACTION_STATUS_RUNNING) { // nothing to do return; } auto& actions = priv->actions; auto& idx = priv->current_action; auto old_finger_state = priv->finger_state; priv->finger_state.update(event); auto next_action = [&] () { ++idx; if (idx < actions.size()) { actions[idx]->reset(event.time); priv->finger_state.reset_origin(); } }; /** Go through all ALREADY_COMPLETED gestures */ action_status_t status; while (idx < actions.size()) { status = actions[idx]->update_state(priv->finger_state, event); if (status == ACTION_STATUS_ALREADY_COMPLETED) { /* Make sure that the previous finger state is marked as origin, * because the last update is not consumed by the last action */ priv->finger_state = old_finger_state; next_action(); priv->finger_state.update(event); } else { break; } } switch (status) { case ACTION_STATUS_RUNNING: return; // nothing more to do case ACTION_STATUS_CANCELLED: priv->status = ACTION_STATUS_CANCELLED; break; case ACTION_STATUS_ALREADY_COMPLETED: // fallthrough case ACTION_STATUS_COMPLETED: if (idx < actions.size()) { next_action(); } if (idx == actions.size()) { priv->status = ACTION_STATUS_COMPLETED; } break; } if (priv->status == ACTION_STATUS_CANCELLED) { priv->cancelled(); } if (priv->status == ACTION_STATUS_COMPLETED) { priv->completed(); } } void wf::touch::gesture_t::reset(uint32_t time) { priv->status = ACTION_STATUS_RUNNING; priv->finger_state.fingers.clear(); priv->current_action = 0; priv->actions[0]->reset(time); } wayfire-0.0~git20210319.8974eb0/test/000077500000000000000000000000001402513564700165325ustar00rootroot00000000000000wayfire-0.0~git20210319.8974eb0/test/action_test.cpp000066400000000000000000000126011402513564700215520ustar00rootroot00000000000000#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include #include "shared.hpp" TEST_CASE("touch_action_t") { touch_action_t touch_down{2, true}; touch_down.set_target({0, 0, 10, 10}); touch_down.set_duration(150); touch_down.set_move_tolerance(5); gesture_event_t event_down; event_down.type = EVENT_TYPE_TOUCH_DOWN; event_down.time = 75; // check normal operation, with tolerance gesture_state_t state; state.fingers[0] = finger_2p(0, 0, 0, 0); touch_down.reset(0); CHECK(touch_down.update_state(state, event_down) == ACTION_STATUS_RUNNING); gesture_event_t motion; motion.type = EVENT_TYPE_MOTION; motion.finger = 0; motion.time = 100; motion.pos = {1, 1}; state.fingers[0] = finger_2p(0, 0, 1, 1); CHECK(touch_down.update_state(state, motion) == ACTION_STATUS_RUNNING); state.fingers[1] = finger_in_dir(2, 2); event_down.finger = 2; event_down.pos = {2, 2}; event_down.time = 150; CHECK(touch_down.update_state(state, event_down) == ACTION_STATUS_COMPLETED); // check outside of bounds state.fingers[0] = finger_2p(15, 15, 20, 20); touch_down.reset(0); CHECK(touch_down.update_state(state, event_down) == ACTION_STATUS_CANCELLED); state.fingers[0] = finger_2p(0, 0, 0, 0); // check timeout event_down.time = 151; touch_down.reset(0); CHECK(touch_down.update_state(state, event_down) == ACTION_STATUS_CANCELLED); touch_action_t touch_up{2, false}; gesture_event_t event_up; event_up.type = EVENT_TYPE_TOUCH_UP; event_up.time = 150; // start touch up action state.fingers[1] = finger_2p(2, 2, 3, 3); touch_up.reset(0); CHECK(touch_up.update_state(state, event_up) == ACTION_STATUS_RUNNING); // complete it state.fingers.erase(1); CHECK(touch_up.update_state(state, event_up) == ACTION_STATUS_COMPLETED); // check tolerance exceeded state.fingers[1] = finger_2p(2, 2, 2, 3); touch_up.set_move_tolerance(0); touch_up.reset(0); CHECK(touch_up.update_state(state, event_up) == ACTION_STATUS_CANCELLED); } TEST_CASE("wf::touch::hold_action_t") { hold_action_t hold{50}; hold.set_move_tolerance(1); gesture_state_t state; state.fingers[0] = finger_in_dir(1, 0); gesture_event_t ev; // check ok state hold.reset(0); ev.time = 49; ev.type = EVENT_TYPE_MOTION; CHECK(hold.update_state(state, ev) == ACTION_STATUS_RUNNING); ev.time = 50; ev.type = EVENT_TYPE_TOUCH_UP; CHECK(hold.update_state(state, ev) == ACTION_STATUS_ALREADY_COMPLETED); // check finger breaks action hold.reset(0); ev.type = EVENT_TYPE_TOUCH_UP; ev.time = 49; CHECK(hold.update_state(state, ev) == ACTION_STATUS_CANCELLED); // check too much movement state.fingers[0] = finger_in_dir(2, 0); ev.time = 49; hold.reset(0); CHECK(hold.update_state(state, ev) == ACTION_STATUS_CANCELLED); } TEST_CASE("wf::touch::drag_action_t") { drag_action_t drag{MOVE_DIRECTION_LEFT, 50}; drag.set_move_tolerance(5); gesture_state_t state; state.fingers[0] = finger_in_dir(-50, 0); state.fingers[1] = finger_in_dir(-50, 3); gesture_event_t ev; ev.type = EVENT_TYPE_MOTION; ev.time = 0; // check ok drag.reset(0); CHECK(drag.update_state(state, ev) == ACTION_STATUS_COMPLETED); // check distance not enough drag.reset(0); state.fingers[0] = finger_in_dir(-49, 0); CHECK(drag.update_state(state, ev) == ACTION_STATUS_RUNNING); // check exceeds tolerance state.fingers[1] = finger_in_dir(0, 6); drag.reset(0); CHECK(drag.update_state(state, ev) == ACTION_STATUS_CANCELLED); // check touch cancels ev.type = EVENT_TYPE_TOUCH_UP; state.fingers[1] = finger_in_dir(-50, 3); drag.reset(0); CHECK(drag.update_state(state, ev) == ACTION_STATUS_CANCELLED); } TEST_CASE("wf::touch::pinch_action_t") { pinch_action_t in{0.5}, out{2}; gesture_state_t state; state.fingers[0] = finger_2p(1, 0, 2, 1); state.fingers[1] = finger_2p(-1, -2, -3, -4); gesture_event_t ev; ev.time = 0; ev.type = EVENT_TYPE_MOTION; // ok out.reset(0); CHECK(out.update_state(state, ev) == ACTION_STATUS_COMPLETED); std::swap(state.fingers[0].origin, state.fingers[0].current); std::swap(state.fingers[1].origin, state.fingers[1].current); in.reset(0); CHECK(in.update_state(state, ev) == ACTION_STATUS_COMPLETED); // too much movement in.set_move_tolerance(1); in.reset(0); state.fingers[0].current += point_t{2, 0}; state.fingers[1].current += point_t{2, 0}; CHECK(in.update_state(state, ev) == ACTION_STATUS_CANCELLED); // touch cancels in.reset(0); state.fingers[0].current -= point_t{2, 0}; state.fingers[1].current -= point_t{2, 0}; ev.type = EVENT_TYPE_TOUCH_DOWN; CHECK(in.update_state(state, ev) == ACTION_STATUS_CANCELLED); } TEST_CASE("wf::touch::rotate_action_t") { gesture_state_t state; state.fingers[0] = finger_2p(0, 1, 1, 0); state.fingers[1] = finger_2p(1, 0, 0, -1); state.fingers[2] = finger_2p(0, -1, -1, 0); state.fingers[3] = finger_2p(-1, 0, 0, 1); CHECK(state.get_rotation_angle() == doctest::Approx(-M_PI / 2.0)); rotate_action_t rotate{-M_PI / 3.0}; gesture_event_t ev; ev.type = EVENT_TYPE_MOTION; CHECK(rotate.update_state(state, ev) == ACTION_STATUS_COMPLETED); // TODO: incomplete tests } wayfire-0.0~git20210319.8974eb0/test/basic_test.cpp000066400000000000000000000122271402513564700213620ustar00rootroot00000000000000#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include #include "shared.hpp" TEST_CASE("get_move_in_direction") { CHECK(finger_in_dir(1, 0).get_direction() == MOVE_DIRECTION_RIGHT); CHECK(finger_in_dir(-1, -1).get_direction() == lu); CHECK(finger_in_dir(1, 1).get_direction() == rd); CHECK(finger_in_dir(0, 0).get_direction() == 0); CHECK(finger_in_dir(-10, 1).get_direction() == MOVE_DIRECTION_LEFT); } TEST_CASE("get_drag_distance") { CHECK(finger_in_dir(0, 5).get_drag_distance(MOVE_DIRECTION_DOWN) == doctest::Approx(5)); CHECK(finger_in_dir(-1, -1).get_drag_distance(MOVE_DIRECTION_DOWN) == doctest::Approx(0)); } TEST_CASE("get_incorrect_drag_distance") { CHECK(finger_in_dir(-1, -1).get_incorrect_drag_distance(lu) == doctest::Approx(0)); CHECK(finger_in_dir(-1, -1).get_incorrect_drag_distance(ru) == doctest::Approx(std::sqrt(2))); CHECK(finger_in_dir(-1, -1).get_incorrect_drag_distance(ld) == doctest::Approx(std::sqrt(2))); CHECK(finger_in_dir(5, 5).get_incorrect_drag_distance(MOVE_DIRECTION_RIGHT) == doctest::Approx(5)); CHECK(finger_in_dir(4, 0).get_incorrect_drag_distance(MOVE_DIRECTION_LEFT) == doctest::Approx(4)); } TEST_CASE("get_pinch_scale") { gesture_state_t state; state.fingers[0] = finger_2p(1, 0, 2, 1); state.fingers[1] = finger_2p(-1, -2, -3, -4); CHECK(state.get_pinch_scale() > 2); std::swap(state.fingers[0].origin, state.fingers[0].current); std::swap(state.fingers[1].origin, state.fingers[1].current); CHECK(state.get_pinch_scale() < 0.5); state.fingers[0] = finger_2p(1, 1, 1, 1); state.fingers[1] = finger_2p(2, 2, 2, 2); CHECK(state.get_pinch_scale() == doctest::Approx(1)); } TEST_CASE("get_rotation_angle") { gesture_state_t state; state.fingers[0] = finger_2p(0, 1, 1, 0); state.fingers[1] = finger_2p(1, 0, 0, -1); state.fingers[2] = finger_2p(0, -1, -1, 0); state.fingers[3] = finger_2p(-1, 0, 0, 1); CHECK(state.get_rotation_angle() == doctest::Approx(-M_PI / 2.0)); // triangle (0, 0), (56, 15), (15, 56) is almost equilateral state.fingers.clear(); state.fingers[0] = finger_2p(0, 0, 56, 15); state.fingers[1] = finger_2p(56, 15, 15, 56); state.fingers[2] = finger_2p(15, 56, 0, 0); CHECK(state.get_rotation_angle() == doctest::Approx(2.0 * M_PI / 3.0).epsilon(0.05)); } TEST_CASE("finger_t") { CHECK(finger_in_dir(1, 1).delta() == point_t{1, 1}); } static void compare_point(const point_t& a, const point_t& b) { CHECK(a.x == doctest::Approx(b.x)); CHECK(a.y == doctest::Approx(b.y)); } static void compare_finger(const finger_t& a, const finger_t& b) { compare_point(a.origin, b.origin); compare_point(a.current, b.current); } TEST_CASE("gesture_state_t") { gesture_state_t state; state.fingers[0] = finger_in_dir(1, 2); state.fingers[1] = finger_in_dir(3, 4); state.fingers[2] = finger_in_dir(5, 6); compare_finger(state.get_center(), finger_in_dir(3, 4)); } TEST_CASE("gesture_state_t::update") { gesture_state_t state; gesture_event_t ev; ev.finger = 0; ev.pos = {4, 5}; ev.type = EVENT_TYPE_TOUCH_DOWN; state.update(ev); CHECK(state.fingers.size() == 1); compare_finger(state.fingers[0], finger_2p(4, 5, 4, 5)); ev.finger = 0; ev.pos = {6, 7}; ev.type = EVENT_TYPE_MOTION; state.update(ev); CHECK(state.fingers.size() == 1); compare_finger(state.fingers[0], finger_2p(4, 5, 6, 7)); ev.finger = 1; ev.pos = {7, -1}; ev.type = EVENT_TYPE_TOUCH_DOWN; state.update(ev); CHECK(state.fingers.size() == 2); compare_finger(state.fingers[0], finger_2p(4, 5, 6, 7)); compare_finger(state.fingers[1], finger_2p(7, -1, 7, -1)); ev.type = EVENT_TYPE_TOUCH_UP; ev.finger = 0; state.update(ev); CHECK(state.fingers.size() == 1); compare_finger(state.fingers[1], finger_2p(7, -1, 7, -1)); } TEST_CASE("gesture_state_t::reset_origin") { gesture_state_t state; state.fingers[0] = finger_in_dir(6, 7); state.reset_origin(); CHECK(state.fingers.size() == 1); compare_finger(state.fingers[0], finger_2p(6, 7, 6, 7)); } class action_test_t : public gesture_action_t { public: action_test_t() {} uint32_t get_start_time() { return this->start_time; } virtual action_status_t update_state(const gesture_state_t& state, const gesture_event_t& event) { return calculate_next_status(state, event, true); } }; TEST_CASE("gesture_action_t") { action_test_t test; test.set_move_tolerance(5.0); test.set_duration(20); CHECK(test.get_move_tolerance() == 5.0); CHECK(test.get_duration() == 20); gesture_state_t state; gesture_event_t event; event.time = 20; test.reset(0); CHECK(test.update_state(state, event) == ACTION_STATUS_RUNNING); event.time = 21; CHECK(test.update_state(state, event) == ACTION_STATUS_CANCELLED); } TEST_CASE("touch_target_t") { touch_target_t target{-1, 1, 2, 2}; CHECK(target.contains({0, 2})); CHECK(target.contains({-1, 1})); CHECK(!target.contains({1, 3})); CHECK(!target.contains({0, 5})); } wayfire-0.0~git20210319.8974eb0/test/gesture_test.cpp000066400000000000000000000045161402513564700217610ustar00rootroot00000000000000#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include #include using namespace wf::touch; TEST_CASE("wf::touch::gesture_t") { int completed = 0; int cancelled = 0; gesture_callback_t callback1 = [&] () { ++completed; }; gesture_callback_t callback2 = [&] () { ++cancelled; }; auto down = std::make_unique(1, true); auto delay1 = std::make_unique(5); auto drag1 = std::make_unique(MOVE_DIRECTION_LEFT, 10); auto delay2 = std::make_unique(5); auto drag2 = std::make_unique(MOVE_DIRECTION_RIGHT, 10); std::vector> actions; actions.emplace_back(std::move(down)); actions.emplace_back(std::move(delay1)); actions.emplace_back(std::move(drag1)); actions.emplace_back(std::move(delay2)); actions.emplace_back(std::move(drag2)); gesture_t swipe{std::move(actions), callback1, callback2}; swipe.reset(0); gesture_event_t touch_down; touch_down.finger = 0; touch_down.pos = {0, 0}; touch_down.type = EVENT_TYPE_TOUCH_DOWN; touch_down.time = 0; swipe.update_state(touch_down); CHECK(swipe.get_progress() >= 0.2); SUBCASE("complete") { gesture_event_t motion_left; motion_left.finger = 0; motion_left.pos = {-10, 0}; motion_left.time = 10; motion_left.type = EVENT_TYPE_MOTION; swipe.update_state(motion_left); CHECK(cancelled == 0); CHECK(completed == 0); CHECK(swipe.get_progress() >= 0.6); gesture_event_t motion_right = motion_left; motion_right.pos = {0, 0}; motion_right.time = 20; swipe.update_state(motion_right); CHECK(cancelled == 0); CHECK(completed == 1); SUBCASE("restart") { swipe.reset(0); CHECK(swipe.get_progress() == 0.0); swipe.update_state(touch_down); swipe.update_state(motion_left); swipe.update_state(motion_right); CHECK(cancelled == 0); CHECK(completed == 2); } } SUBCASE("cancelled") { touch_down.finger = 1; swipe.update_state(touch_down); CHECK(cancelled == 1); CHECK(completed == 0); } } wayfire-0.0~git20210319.8974eb0/test/meson.build000066400000000000000000000007371402513564700207030ustar00rootroot00000000000000basic_test = executable( 'basic_test', 'basic_test.cpp', dependencies: [wftouch, doctest], install: false) test('Basic test', basic_test) action_test = executable( 'action_test', 'action_test.cpp', dependencies: [wftouch, doctest], install: false) test('Action test', action_test) gesture_test = executable( 'gesture_test', 'gesture_test.cpp', dependencies: [wftouch, doctest], install: false) test('Gesture test', gesture_test) wayfire-0.0~git20210319.8974eb0/test/shared.hpp000066400000000000000000000012051402513564700205070ustar00rootroot00000000000000#define _USE_MATH_DEFINES #include #include using namespace wf::touch; static finger_t finger_in_dir(double x, double y) { return finger_t { .origin = {0, 0}, .current = {x, y} }; } const uint32_t lu = MOVE_DIRECTION_LEFT | MOVE_DIRECTION_UP; const uint32_t ld = MOVE_DIRECTION_LEFT | MOVE_DIRECTION_DOWN; const uint32_t rd = MOVE_DIRECTION_RIGHT | MOVE_DIRECTION_DOWN; const uint32_t ru = MOVE_DIRECTION_RIGHT | MOVE_DIRECTION_UP; static finger_t finger_2p(double x, double y, double a, double b) { return finger_t { .origin = {x, y}, .current = {a, b} }; } wayfire-0.0~git20210319.8974eb0/wayfire/000077500000000000000000000000001402513564700172215ustar00rootroot00000000000000wayfire-0.0~git20210319.8974eb0/wayfire/touch/000077500000000000000000000000001402513564700203435ustar00rootroot00000000000000wayfire-0.0~git20210319.8974eb0/wayfire/touch/touch.hpp000066400000000000000000000261611402513564700222040ustar00rootroot00000000000000#pragma once /** * Touchscreen gesture library, designed for use in Wayfire (and elsewhere). * Goal is to process touch events and detect various configurable gestures. * * High-level design: * A gesture consists of one or more consecutive actions. * * An action is usually a simple part of the gesture which can be processed * separately, for ex. touch down with 3 fingers, swipe in a direction, etc. * * When processing events, the gesture starts with its first action. Once it is * completed, the processing continues with the next action, and so on, until * either all actions are completed or an action cancels the gesture. */ #include #include #include #include #include namespace wf { namespace touch { using point_t = glm::dvec2; /** * Movement direction. */ enum move_direction_t { MOVE_DIRECTION_LEFT = (1 << 0), MOVE_DIRECTION_RIGHT = (1 << 1), MOVE_DIRECTION_UP = (1 << 2), MOVE_DIRECTION_DOWN = (1 << 3), }; struct finger_t { point_t origin; point_t current; /** Get movement vector */ point_t delta() const; /** Find direction of movement, a bitmask of move_direction_t */ uint32_t get_direction() const; /** Find drag distance in the given direction */ double get_drag_distance(uint32_t direction) const; /** Find drag distance in opposite and perpendicular directions */ double get_incorrect_drag_distance(uint32_t direction) const; }; enum gesture_event_type_t { /** Finger touched down the screen */ EVENT_TYPE_TOUCH_DOWN, /** Finger was lifted off the screen */ EVENT_TYPE_TOUCH_UP, /** Finger moved across the screen */ EVENT_TYPE_MOTION, }; /** * Represents a single update on the touch state. */ struct gesture_event_t { /** type of the event */ gesture_event_type_t type; /** timestamp of the event in milliseconds */ uint32_t time; /** finger id which the event is about */ int32_t finger; /** coordinates of the finger */ point_t pos; }; /** * Contains all fingers. */ struct gesture_state_t { public: // finger_id -> finger_t std::map fingers; /** Update fingers based on the event */ void update(const gesture_event_t& event); /** Reset finger origin to current positions */ void reset_origin(); /** Find the center points of the fingers. */ finger_t get_center() const; /** Get the pinch scale of current touch points. */ double get_pinch_scale() const; /** * Get the rotation angle in radians of current touch points. * NB: Works only for rotation < 180 degrees. */ double get_rotation_angle() const; }; /** * Represents the status of an action after it is updated */ enum action_status_t { /** Action is done after this event. */ ACTION_STATUS_COMPLETED, /** Action was completed before this event (for example, hold action). */ ACTION_STATUS_ALREADY_COMPLETED, /** Action is still running after this event. */ ACTION_STATUS_RUNNING, /** The whole gesture should be cancelled. */ ACTION_STATUS_CANCELLED, }; /** * Represents a part of the gesture. */ class gesture_action_t { public: /** * Set the move tolerance. * This is the maximum amount the fingers may move in unwanted directions. */ void set_move_tolerance(double tolerance); /** @return The move tolerance. */ double get_move_tolerance() const; /** * Set the duration of the action in milliseconds. * This is the maximal time needed for this action to be happening to * consider it complete. */ void set_duration(uint32_t duration); /** @return The duration of the gesture action. */ uint32_t get_duration() const; /** * Update the action's state according to the new state. * * NOTE: The actual implementation should update the @start_time field. * * @param state The gesture state since the last reset of the gesture. * @param event The event causing this update. * @return The new action status. */ virtual action_status_t update_state(const gesture_state_t& state, const gesture_event_t& event) = 0; /** * Reset the action. * Called whenever the action is started again. */ virtual void reset(uint32_t time); virtual ~gesture_action_t() {} protected: gesture_action_t() {} /** Time of the first event. */ int64_t start_time; /** * Calculate the correct action status. It is determined as follows: * 1. action has timed out(i.e start_time + duration > timestamp) => CANCELLED * 1. finger movement exceeds move tolerance => CANCELLED * 2. @running is false and gesture has not timed out => COMPLETED * 3. @running is true and gesture has not timed out => RUNNING */ action_status_t calculate_next_status(const gesture_state_t& state, const gesture_event_t& last_event, bool running); /** * Calculate whether movement exceeds tolerance. * By default, tolerance is ignored, so actions should override this function. */ virtual bool exceeds_tolerance(const gesture_state_t& state); private: double tolerance = 1e18; // very big uint32_t duration = -1; // maximal duration }; /** * Represents a target area where the touch event takes place. */ struct touch_target_t { double x; double y; double width; double height; bool contains(const point_t& point) const; }; /** * Represents the action of touching down with several fingers. */ class touch_action_t : public gesture_action_t { public: /** * Create a new touch down or up action. * * @param cnt_fingers The number of fingers that need to be touched down * or released to consider the action completed. * @param touch_down Whether the action is touch down or touch up. */ touch_action_t(int cnt_fingers, bool touch_down); /** * Set the target area of this gesture. */ void set_target(const touch_target_t& target); /** * Mark the action as completed iff state has the right amount of fingers * and if the event is a touch down. */ action_status_t update_state(const gesture_state_t& state, const gesture_event_t& event) override; void reset(uint32_t time) override; protected: /** @return True if the fingers have moved too much. */ bool exceeds_tolerance(const gesture_state_t& state) override; private: int cnt_fingers; int released_fingers; gesture_event_type_t type; touch_target_t target; }; /** * Represents the action of holding the fingers still for a certain amount * of time. */ class hold_action_t : public gesture_action_t { public: /** * Create a new hold action. * * @param threshold The time is milliseconds needed to consider the gesture * complete. */ hold_action_t(int32_t threshold); /** * The action is already completed iff no fingers have been added or * released and the given amount of time has passed without much movement. */ action_status_t update_state(const gesture_state_t& state, const gesture_event_t& event) override; protected: /** @return True if the fingers have moved too much. */ bool exceeds_tolerance(const gesture_state_t& state) override; private: int32_t threshold; }; /** * Represents the action of dragging the fingers in a particular direction * over a particular distance. */ class drag_action_t : public gesture_action_t { public: /** * Create a new drag action. * * @param direction The direction of the drag action. * @param threshold The distance that needs to be covered. */ drag_action_t(uint32_t direction, double threshold); /** * The action is already completed iff no fingers have been added or * released and the given amount of time has passed without much movement. */ action_status_t update_state(const gesture_state_t& state, const gesture_event_t& event) override; protected: /** * @return True if any finger has moved more than the threshold in an * incorrect direction. */ bool exceeds_tolerance(const gesture_state_t& state) override; private: double threshold; uint32_t direction; }; /** * Represents a pinch action. */ class pinch_action_t : public gesture_action_t { public: /** * Create a new pinch action. * * @param threshold The threshold to be exceeded. * If threshold is less/more than 1, then the action is complete when * the actual pinch scale is respectively less/more than threshold. */ pinch_action_t(double threshold); /** * The action is already completed iff no fingers have been added or * released and the pinch threshold has been reached without much movement. */ action_status_t update_state(const gesture_state_t& state, const gesture_event_t& event) override; protected: /** * @return True if gesture center has moved more than tolerance. */ bool exceeds_tolerance(const gesture_state_t& state) override; private: double threshold; }; /** * Represents a rotate action. */ class rotate_action_t : public gesture_action_t { public: /** * Create a new rotate action. * * @param threshold The threshold to be exceeded. * If threshold is less/more than 0, then the action is complete when * the actual rotation angle is respectively less/more than threshold. */ rotate_action_t(double threshold); /** * The action is already completed iff no fingers have been added or * released and the rotation threshold has been reached without much movement. */ action_status_t update_state(const gesture_state_t& state, const gesture_event_t& event) override; protected: /** * @return True if gesture center has moved more than tolerance. */ bool exceeds_tolerance(const gesture_state_t& state) override; private: double threshold; }; using gesture_callback_t = std::function; /** * Represents a series of actions forming a gesture together. */ class gesture_t { public: /** * Create a new gesture consisting of the given actions. * * @param actions The actions the gesture consists of. * @param completed The callback to execute each time the gesture is * completed. * @param cancelled The callback to execute each time the gesture is * cancelled. */ gesture_t(std::vector> actions, gesture_callback_t completed, gesture_callback_t cancelled = [](){}); ~gesture_t(); /** @return What percentage of the actions are complete. */ double get_progress() const; /** * Update the gesture state. * * @param event The next event. */ void update_state(const gesture_event_t& event); /** * Reset the gesture state. * * @param time The time of the event causing the start of gesture * recognition, this is typically the first touch event. */ void reset(uint32_t time); private: class impl; std::unique_ptr priv; }; } }