pax_global_header00006660000000000000000000000064146121426220014512gustar00rootroot0000000000000052 comment=caa156921c6be1dff9c2ccd851330c96de7928bf wayfire-0.0~git20240424.caa1569/000077500000000000000000000000001461214262200156135ustar00rootroot00000000000000wayfire-0.0~git20240424.caa1569/LICENSE000066400000000000000000000020501461214262200166150ustar00rootroot00000000000000MIT 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~git20240424.caa1569/README.md000066400000000000000000000003461461214262200170750ustar00rootroot00000000000000# 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~git20240424.caa1569/meson.build000066400000000000000000000014661461214262200177640ustar00rootroot00000000000000project('wf-touch', ['cpp'], version : '0.0', meson_version: '>=0.47.0', default_options : ['cpp_std=c++17']) glm = dependency('glm', required: false) if not glm.found() and not meson.get_compiler('cpp').check_header('glm/glm.hpp') error('GLM not found, and directly using the header \'glm/glm.hpp\' is not possible.') endif 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~git20240424.caa1569/meson_options.txt000066400000000000000000000001221461214262200212430ustar00rootroot00000000000000option('tests', type: 'feature', value: 'auto', description: 'Enable unit tests') wayfire-0.0~git20240424.caa1569/src/000077500000000000000000000000001461214262200164025ustar00rootroot00000000000000wayfire-0.0~git20240424.caa1569/src/actions.cpp000066400000000000000000000135641461214262200205570ustar00rootroot00000000000000#include #include 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; } wf::touch::touch_action_t& wf::touch::touch_action_t::set_target(const touch_target_t& target) { this->target = target; return *this; } 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->move_tolerance; } void wf::touch::touch_action_t::reset(uint32_t time) { gesture_action_t::reset(time); this->cnt_touch_events = 0; } action_status_t wf::touch::touch_action_t::update_state( const gesture_state_t& state, const gesture_event_t& event) { if (exceeds_tolerance(state)) { return ACTION_STATUS_CANCELLED; } switch (event.type) { case EVENT_TYPE_MOTION: return ACTION_STATUS_RUNNING; case EVENT_TYPE_TIMEOUT: return ACTION_STATUS_CANCELLED; case EVENT_TYPE_TOUCH_UP: // fallthrough case EVENT_TYPE_TOUCH_DOWN: if (this->type != event.type) { // down when we want up or vice versa 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; } } this->cnt_touch_events++; if (this->cnt_touch_events == this->cnt_fingers) { return ACTION_STATUS_COMPLETED; } else { return ACTION_STATUS_RUNNING; } } return ACTION_STATUS_RUNNING; } /*- -------------------------- Hold action ---------------------------------- */ wf::touch::hold_action_t::hold_action_t(int32_t threshold) { set_duration(threshold); } action_status_t wf::touch::hold_action_t::update_state(const gesture_state_t& state, const gesture_event_t& event) { switch (event.type) { case EVENT_TYPE_MOTION: if (exceeds_tolerance(state)) { return ACTION_STATUS_CANCELLED; } else { return ACTION_STATUS_RUNNING; } case EVENT_TYPE_TIMEOUT: return ACTION_STATUS_COMPLETED; default: return ACTION_STATUS_CANCELLED; } } bool wf::touch::hold_action_t::exceeds_tolerance(const gesture_state_t& state) { return find_max_delta(state) > this->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; } if (exceeds_tolerance(state)) { return ACTION_STATUS_CANCELLED; } const double dragged = state.get_center().get_drag_distance(this->direction); if (dragged >= this->threshold) { return ACTION_STATUS_COMPLETED; } else { return ACTION_STATUS_RUNNING; } } 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) > 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; } if (exceeds_tolerance(state)) { return ACTION_STATUS_CANCELLED; } const double current_scale = state.get_pinch_scale(); if (((this->threshold < 1.0) && (current_scale <= threshold)) || ((this->threshold > 1.0) && (current_scale >= threshold))) { return ACTION_STATUS_COMPLETED; } return ACTION_STATUS_RUNNING; } bool wf::touch::pinch_action_t::exceeds_tolerance(const gesture_state_t& state) { return glm::length(state.get_center().delta()) > this->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; } if (exceeds_tolerance(state)) { return ACTION_STATUS_CANCELLED; } const double current_scale = state.get_rotation_angle(); if (((this->threshold < 0.0) && (current_scale <= threshold)) || ((this->threshold > 0.0) && (current_scale >= threshold))) { return ACTION_STATUS_COMPLETED; } return ACTION_STATUS_RUNNING; } bool wf::touch::rotate_action_t::exceeds_tolerance(const gesture_state_t& state) { return glm::length(state.get_center().delta()) > this->move_tolerance; } wayfire-0.0~git20240424.caa1569/src/math.cpp000066400000000000000000000067221461214262200200460ustar00rootroot00000000000000#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~git20240424.caa1569/src/touch.cpp000066400000000000000000000127621461214262200202400ustar00rootroot00000000000000#include 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; default: break; } } void wf::touch::gesture_state_t::reset_origin() { for (auto& f : fingers) { f.second.origin = f.second.current; } } wf::touch::gesture_action_t& wf::touch::gesture_action_t::set_duration(uint32_t duration) { this->duration = duration; return *this; } std::optional wf::touch::gesture_action_t::get_duration() const { return this->duration; } 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; std::unique_ptr timer; void start_gesture(uint32_t time) { status = ACTION_STATUS_RUNNING; finger_state.fingers.clear(); current_action = 0; actions[0]->reset(time); start_timer(); } void start_timer() { if (auto dur = actions[current_action]->get_duration()) { timer->set_timeout(*dur, [=] () { update_state(gesture_event_t{.type = EVENT_TYPE_TIMEOUT}); }); } } void update_state(const gesture_event_t& event) { if (status != ACTION_STATUS_RUNNING) { // nothing to do return; } auto& idx = current_action; auto old_finger_state = finger_state; finger_state.update(event); auto next_action = [&] () -> bool { timer->reset(); ++idx; if (idx < actions.size()) { actions[idx]->reset(event.time); finger_state.reset_origin(); start_timer(); return true; } return false; }; action_status_t pending_status = actions[idx]->update_state(finger_state, event); switch (pending_status) { case ACTION_STATUS_RUNNING: return; // nothing more to do case ACTION_STATUS_CANCELLED: this->status = ACTION_STATUS_CANCELLED; timer->reset(); cancelled(); return; case ACTION_STATUS_COMPLETED: bool has_next = next_action(); if (!has_next) { this->status = ACTION_STATUS_COMPLETED; completed(); return; } } } }; void wf::touch::gesture_t::set_timer(std::unique_ptr timer) { priv->timer = std::move(timer); } wf::touch::gesture_t::gesture_t(std::vector> actions, gesture_callback_t completed, gesture_callback_t cancelled) { this->priv = std::make_unique(); priv->actions = std::move(actions); priv->completed = completed; priv->cancelled = cancelled; } wf::touch::gesture_t::gesture_t(gesture_t&& other) { this->priv = std::move(other.priv); } wf::touch::gesture_t& wf::touch::gesture_t::operator=(gesture_t&& other) { this->priv = std::move(other.priv); return *this; } 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) { assert(priv->timer); assert(!priv->actions.empty()); priv->update_state(event); } wf::touch::action_status_t wf::touch::gesture_t::get_status() const { return priv->status; } void wf::touch::gesture_t::reset(uint32_t time) { assert(priv->timer); assert(!priv->actions.empty()); if (priv->status == ACTION_STATUS_RUNNING) { return; } priv->start_gesture(time); } wf::touch::gesture_builder_t::gesture_builder_t() {} wf::touch::gesture_builder_t& wf::touch::gesture_builder_t::on_completed(gesture_callback_t callback) { this->_on_completed = callback; return *this; } wf::touch::gesture_builder_t& wf::touch::gesture_builder_t::on_cancelled(gesture_callback_t callback) { this->_on_cancelled = callback; return *this; } wf::touch::gesture_t wf::touch::gesture_builder_t::build() { return gesture_t(std::move(actions), _on_completed, _on_cancelled); } wayfire-0.0~git20240424.caa1569/test/000077500000000000000000000000001461214262200165725ustar00rootroot00000000000000wayfire-0.0~git20240424.caa1569/test/action_test.cpp000066400000000000000000000125731461214262200216220ustar00rootroot00000000000000#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 touch_down.reset(0); CHECK(touch_down.update_state(state, gesture_event_t{.type = EVENT_TYPE_TIMEOUT}) == 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); CHECK(hold.update_state(state, gesture_event_t{.type = EVENT_TYPE_TIMEOUT}) == ACTION_STATUS_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~git20240424.caa1569/test/basic_test.cpp000066400000000000000000000105671461214262200214270ustar00rootroot00000000000000#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)); } 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~git20240424.caa1569/test/gesture_test.cpp000066400000000000000000000130031461214262200220100ustar00rootroot00000000000000#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN #include #include using namespace wf::touch; class fake_timer_t : public timer_interface_t { public: std::vector requests; std::function last_cb; void set_timeout(uint32_t req, std::function cb) override { requests.push_back(req); last_cb = cb; } void reset() override { requests.push_back(-1); } }; TEST_CASE("wf::touch::gesture_t") { int completed = 0; int cancelled = 0; gesture_callback_t callback1 = [&] () { ++completed; }; gesture_callback_t callback2 = [&] () { ++cancelled; }; auto _timer = std::make_unique(); auto timer_ptr = _timer.get(); #define REQUIRE_TIMERS(...) CHECK(timer_ptr->requests == std::vector{__VA_ARGS__}); SUBCASE("Hold gesture") { gesture_t hold = gesture_builder_t() .action(touch_action_t(2, true).set_duration(100)) .action(hold_action_t(200)) .on_completed(callback1) .on_cancelled(callback2) .build(); hold.set_timer(std::move(_timer)); hold.reset(0); hold.update_state( gesture_event_t{ .type = EVENT_TYPE_TOUCH_DOWN, .time = 0, .finger = 0, .pos = {0, 0}}); REQUIRE_TIMERS(100); SUBCASE("Timeout") { timer_ptr->last_cb(); CHECK(hold.get_status() == ACTION_STATUS_CANCELLED); } SUBCASE("OK") { hold.update_state( gesture_event_t{ .type = EVENT_TYPE_TOUCH_DOWN, .time = 10, .finger = 1, .pos = {0, 0}}); REQUIRE_TIMERS(100, -1, 200); timer_ptr->last_cb(); CHECK(hold.get_status() == ACTION_STATUS_COMPLETED); REQUIRE_TIMERS(100, -1, 200, -1); } } SUBCASE("double-tap gesture") { gesture_t double_tap = gesture_builder_t() .action(touch_action_t(1, true).set_duration(100)) .action(touch_action_t(1, false).set_duration(100)) .action(touch_action_t(1, true).set_duration(100)) .action(touch_action_t(1, false)) .on_completed(callback1) .on_cancelled(callback2) .build(); double_tap.set_timer(std::move(_timer)); double_tap.reset(0); double_tap.update_state({.type = EVENT_TYPE_TOUCH_DOWN, .time = 0, .finger = 0, .pos = {0, 0}}); double_tap.update_state({.type = EVENT_TYPE_TOUCH_UP, .time = 20, .finger = 0, .pos = {0, 0}}); CHECK(double_tap.get_status() == ACTION_STATUS_RUNNING); SUBCASE("Success") { double_tap.reset(80); double_tap.update_state({.type = EVENT_TYPE_TOUCH_DOWN, .time = 80, .finger = 0, .pos = {0, 0}}); double_tap.update_state({.type = EVENT_TYPE_TOUCH_UP, .time = 90, .finger = 0, .pos = {0, 0}}); CHECK(completed == 1); CHECK(cancelled == 0); } SUBCASE("Timeout") { REQUIRE_TIMERS(100, -1, 100, -1, 100); timer_ptr->last_cb(); CHECK(double_tap.get_status() == ACTION_STATUS_CANCELLED); CHECK(completed == 0); CHECK(cancelled == 1); REQUIRE_TIMERS(100, -1, 100, -1, 100, -1); double_tap.reset(150); CHECK(double_tap.get_status() == ACTION_STATUS_RUNNING); } } SUBCASE("swipe") { gesture_t swipe = gesture_builder_t() .action(touch_action_t(1, true)) .action(hold_action_t(5)) .action(drag_action_t(MOVE_DIRECTION_LEFT, 10)) .action(hold_action_t(5)) .action(drag_action_t(MOVE_DIRECTION_RIGHT, 10)) .on_completed(callback1) .on_cancelled(callback2) .build(); swipe.set_timer(std::move(_timer)); 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") { timer_ptr->last_cb(); 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); timer_ptr->last_cb(); 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); timer_ptr->last_cb(); swipe.update_state(motion_left); timer_ptr->last_cb(); 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~git20240424.caa1569/test/meson.build000066400000000000000000000007371461214262200207430ustar00rootroot00000000000000basic_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~git20240424.caa1569/test/shared.hpp000066400000000000000000000012051461214262200205470ustar00rootroot00000000000000#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~git20240424.caa1569/wayfire/000077500000000000000000000000001461214262200172615ustar00rootroot00000000000000wayfire-0.0~git20240424.caa1569/wayfire/touch/000077500000000000000000000000001461214262200204035ustar00rootroot00000000000000wayfire-0.0~git20240424.caa1569/wayfire/touch/touch.hpp000066400000000000000000000303321461214262200222370ustar00rootroot00000000000000#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 #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, /** Timeout since action start */ EVENT_TYPE_TIMEOUT, }; /** * 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 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 duration of the action in milliseconds. * * After the duration times out, the action will receive * * * This is the maximal time needed for this action to be happening to * consider it complete. * * @return this */ gesture_action_t& set_duration(uint32_t duration); /** @return The duration of the gesture action. */ std::optional 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; private: std::optional duration; // maximal duration }; #define WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(x) \ x& set_move_tolerance(double tolerance) \ { \ this->move_tolerance = tolerance; \ return *this; \ } \ x& set_duration(uint32_t duration) \ { \ gesture_action_t::set_duration(duration); \ return *this; \ } /** * 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); WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(touch_action_t); /** * Set the target area of this gesture. * * @return this */ touch_action_t& 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); private: int cnt_fingers; int cnt_touch_events; gesture_event_type_t type; uint32_t move_tolerance = 1e9; 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); WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(hold_action_t); /** * 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); private: uint32_t move_tolerance = 1e9; }; /** * 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); WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(drag_action_t); /** * 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); private: double threshold; uint32_t direction; uint32_t move_tolerance = 1e9; }; /** * 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); WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(pinch_action_t); /** * 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); private: double threshold; uint32_t move_tolerance = 1e9; }; /** * 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); WFTOUCH_BUILDER_REPEAT_MEMBERS_WITH_CAST(rotate_action_t); /** * 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); private: double threshold; uint32_t move_tolerance = 1e9; }; using gesture_callback_t = std::function; class timer_interface_t { public: virtual void set_timeout(uint32_t msec, std::function handler) = 0; virtual void reset() = 0; virtual ~timer_interface_t() = default; }; /** * 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(gesture_t&& other); gesture_t& operator=(gesture_t&& other); ~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); /** * Get the current state of the gesture. */ action_status_t get_status() const; /** * 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); /** * Set the timer to use for the gesture. * This needs to be called before using the gesture. * * In wayfire, this is usually set by core. */ void set_timer(std::unique_ptr timer); private: class impl; std::unique_ptr priv; }; /** * A helper class to facilitate gesture construction. */ class gesture_builder_t { public: gesture_builder_t(); template gesture_builder_t& action(const ActionType& action) { actions.push_back(std::make_unique(action)); return *this; } gesture_builder_t& on_completed(gesture_callback_t callback); gesture_builder_t& on_cancelled(gesture_callback_t callback); gesture_t build(); private: gesture_callback_t _on_completed = [](){}; gesture_callback_t _on_cancelled = [](){}; std::vector> actions; }; } }