diff --git a/repos/os/run/event_filter.run b/repos/os/run/event_filter.run
index c5d71006df..7a621131fe 100644
--- a/repos/os/run/event_filter.run
+++ b/repos/os/run/event_filter.run
@@ -559,6 +559,130 @@ install_config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/repos/os/src/server/event_filter/README b/repos/os/src/server/event_filter/README
index a2a90a1c0d..ceaddd1daf 100644
--- a/repos/os/src/server/event_filter/README
+++ b/repos/os/src/server/event_filter/README
@@ -96,6 +96,41 @@ one of the following filters:
rectangular area - using the attributes 'xpos', 'ypos', 'width', and
'height' - and the name of the tapped key as 'key' attribute.
+::
+
+ Triggers artificial key sequences or key combos when (multi-)touch gestures
+ are detected. Gesture detection is configured by the following primitives
+ expressed as sub-nodes (with specified default attributes).
+
+ ::
+
+ Triggers when the specified number of fingers touch and hold for at least
+ 'delay_ms' milliseconds. All fingers must reside in a specified area defined
+ by the 'width' and 'height' attributes around the first touch event.
+ When this gesture has been triggered, it translates touch events into
+ relative motion events. This can be used for dragging or scrolling.
+
+ ::
+
+ Triggers when the specified number of fingers move for at least 'distance'
+ in the specified 'direction' and within 'duration_ms' milliseconds.
+ Supported values for the 'direction' attribute are: "up", "down", "left",
+ "right". The rect in which the gesture is valid can be restricted by
+ providing the 'xpos', 'ypos', 'width' and 'height' attributes.
+
+ Key sequences are specified as '' sub-nodes to the gesture
+ primitives. Key combos are specified by nesting the '' sub-nodes. The
+ 'name' attribute specifies the name of the pressed key. An optional 'hold'
+ attribute can be used for postponing the key release event until the touch
+ release. This is useful for dragging gestures; for example:
+
+ !
+ !
+ !
+ !
+ !
+
+
::
Transforms touch and absolute-motion event coordinates by a sequence of
diff --git a/repos/os/src/server/event_filter/main.cc b/repos/os/src/server/event_filter/main.cc
index 3b92639c72..efae700d45 100644
--- a/repos/os/src/server/event_filter/main.cc
+++ b/repos/os/src/server/event_filter/main.cc
@@ -27,6 +27,7 @@
#include
#include
#include
+#include
#include
#include
@@ -263,6 +264,11 @@ struct Event_filter::Main : Source::Factory, Source::Trigger
if (node.type() == Touch_key_source::name())
return *new (_heap) Touch_key_source(owner, node, *this, _heap);
+ if (node.type() == Touch_gesture_source::name())
+ return *new (_heap) Touch_gesture_source(owner, node, *this,
+ _timer_accessor, *this,
+ _heap);
+
warning("unknown <", node.type(), "> input-source node type");
throw Source::Invalid_config();
}
diff --git a/repos/os/src/server/event_filter/source.h b/repos/os/src/server/event_filter/source.h
index 5d1429e1d0..75ff0505ea 100644
--- a/repos/os/src/server/event_filter/source.h
+++ b/repos/os/src/server/event_filter/source.h
@@ -50,7 +50,8 @@ class Event_filter::Source
|| node.type() == "log"
|| node.type() == "transform"
|| node.type() == "touch-click"
- || node.type() == "touch-key";
+ || node.type() == "touch-key"
+ || node.type() == "touch-gesture";
return false;
}
diff --git a/repos/os/src/server/event_filter/touch_gesture_source.h b/repos/os/src/server/event_filter/touch_gesture_source.h
new file mode 100644
index 0000000000..884855df6d
--- /dev/null
+++ b/repos/os/src/server/event_filter/touch_gesture_source.h
@@ -0,0 +1,618 @@
+/*
+ * \brief Input-event source that generates press/release from touch gestures
+ * \author Johannes Schlatow
+ * \date 2025-03-19
+ *
+ * This filter generates artificial key press/release event pairs when touch
+ * gestures are detected.
+ */
+
+/*
+ * Copyright (C) 2025 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#ifndef _EVENT_FILTER__TOUCH_GESTURE_SOURCE_H_
+#define _EVENT_FILTER__TOUCH_GESTURE_SOURCE_H_
+
+/* Genode includes */
+#include
+#include
+#include
+#include
+#include
+
+/* local includes */
+#include
+
+namespace Event_filter { class Touch_gesture_source; }
+
+
+class Event_filter::Touch_gesture_source : public Source, Source::Filter
+{
+ private:
+
+ using Rect = Genode::Rect<>;
+ using Area = Genode::Area<>;
+ using Point = Genode::Point<>;
+ using Microseconds = Genode::Microseconds;
+
+ Owner _owner;
+
+ Source &_source;
+
+ Allocator &_alloc;
+
+ enum State { IDLE, DETECT, TRIGGERED } _state { IDLE };
+
+ /*
+ * Buffer interface used by gestures
+ */
+ struct Buffer_action : Interface
+ {
+ virtual void clear() = 0;
+ virtual void submit(Sink &) = 0;
+ };
+
+ /*
+ * Event buffer for postponing input events
+ */
+ struct Event_buffer : Buffer_action
+ {
+ enum { MAX_EVENTS = 200 };
+
+ Input::Event _events[MAX_EVENTS];
+ unsigned _count { 0 };
+
+ void store(Input::Event const &e)
+ {
+ if (_count < MAX_EVENTS)
+ _events[_count++] = e;
+ }
+
+ /*
+ * Buffer_action interface
+ */
+
+ void clear() override { _count = 0; }
+
+ void submit(Source::Sink & destination) override
+ {
+ for (unsigned i = 0; i < _count; i++)
+ destination.submit(_events[i]);
+ }
+
+ } _buffer { };
+
+ struct Gesture : Interface, private Registry::Element
+ {
+ Buffered_xml _xml;
+
+ State _state { State::IDLE };
+
+ State state() { return _state; }
+
+ /*
+ * - handles touch and touch_release events
+ * - can submit generated events
+ */
+ virtual void handle_event(Sink &, Input::Event const &) = 0;
+
+ /*
+ * - called when filter is in state TRIGGERED
+ * - can submit buffer and clear buffer
+ * - can inject generated events
+ */
+ virtual void generate(Sink &, Buffer_action &) = 0;
+
+ /*
+ * cancel gesture detection
+ */
+ virtual void cancel() = 0;
+
+ static void _emit_from_xml(Source::Sink &destination,
+ Xml_node const &node,
+ bool release)
+ {
+ node.for_each_sub_node("key", [&] (Xml_node const &key) {
+ Input::Keycode code { Input::KEY_UNKNOWN };
+ try {
+ code = key_code_by_name(key.attribute_value("name", Key_name()));
+ } catch (Unknown_key) { }
+
+ if (!release)
+ destination.submit(Input::Press { code });
+
+ _emit_from_xml(destination, key, release);
+
+ bool const hold = key.attribute_value("hold", false);
+ if (release == hold)
+ destination.submit(Input::Release { code });
+ });
+ }
+
+ Gesture(Registry & registry, Allocator & alloc, Xml_node const &xml)
+ : Registry::Element(registry, *this), _xml(alloc, xml) { }
+ };
+
+ /*
+ * Swipe gesture: detects (multi-)finger swipes
+ * - can be limited to a particular direction (up, down, left, right)
+ * - can be limited to a certain rect
+ * - has a minimum distance after which it will be triggered
+ * - has a maximum time after which gesture detection is cancelled
+ */
+ struct Swipe : Gesture
+ {
+ enum { MAX_FINGERS = 3 };
+
+ Timer::Connection &_timer;
+
+ Timer::One_shot_timeout _timeout {
+ _timer, *this, &Swipe::_handle_timeout };
+
+ enum Direction { ANY, UP, DOWN, LEFT, RIGHT };
+
+ struct Attr
+ {
+ Rect rect;
+ unsigned distance;
+ Direction direction;
+ Microseconds duration;
+ unsigned fingers;
+
+ static Direction _direction_from_xml(Xml_node const & node)
+ {
+ String<8> value { "" };
+ value = node.attribute_value("direction", value);
+
+ if (value == "up") return UP;
+ if (value == "down") return DOWN;
+ if (value == "left") return LEFT;
+ if (value == "right") return RIGHT;
+
+ return ANY;
+ }
+
+ static Microseconds _duration_from_xml(Xml_node const & node)
+ {
+ return Microseconds {
+ node.attribute_value("duration_ms", 1000U)*1000 };
+ }
+
+ static Attr from_xml(Xml_node const &node)
+ {
+ return {
+ .rect = Rect::from_xml(node),
+ .distance = node.attribute_value("distance", 100U),
+ .direction = _direction_from_xml(node),
+ .duration = _duration_from_xml(node),
+ .fingers = node.attribute_value("fingers", 1U)
+ };
+ }
+ };
+
+ Attr const _attr;
+
+ /* state */
+ struct Finger
+ {
+ Point last_pos;
+ Direction direction;
+ unsigned distance { 0 };
+
+ Finger(Point p, Direction dir) : last_pos(p), direction(dir) { }
+ };
+
+ Constructible _fingers[MAX_FINGERS];
+
+ void _update_finger(Input::Touch_id id, Point p)
+ {
+ if (id.value >= MAX_FINGERS)
+ return;
+
+ Constructible & finger { _fingers[id.value] };
+
+ if (!finger.constructed())
+ finger.construct(p,_attr.direction);
+ else {
+ auto abs = [] (auto v) { return v >= 0 ? v : -v; };
+
+ Point diff = p - finger->last_pos;
+
+ /* get distance along the intended direction */
+ int distance = 0;
+ switch (finger->direction) {
+ case UP:
+ distance = -diff.y;
+ break;
+ case DOWN:
+ distance = diff.y;
+ break;
+ case LEFT:
+ distance = -diff.x;
+ break;
+ case RIGHT:
+ distance = diff.x;
+ break;
+ case ANY:
+ /* take largest abs value */
+ distance = max(abs(diff.x), abs(diff.y));
+ break;
+ }
+
+ if (distance > 0)
+ finger->distance += distance;
+
+ finger->last_pos = p;
+ }
+ }
+
+ template
+ void _for_each_finger(FN && fn)
+ {
+ for (unsigned i = 0; i < _attr.fingers && i < MAX_FINGERS; i++)
+ if (!fn(_fingers[i])) break;
+ }
+
+ void _handle_timeout(Duration) { cancel(); }
+
+ void cancel() override
+ {
+ if (_state == IDLE)
+ return;
+
+ if (_timeout.scheduled())
+ _timeout.discard();
+
+ _for_each_finger([&] (Constructible & finger) {
+ finger.destruct();
+ return true;
+ });
+
+ _state = State::IDLE;
+ }
+
+
+ bool _detected()
+ {
+ unsigned fingers_okay { 0 };
+ _for_each_finger([&] (Constructible & finger) {
+ if (!finger.constructed()) return true;
+
+ if (finger->distance >= _attr.distance)
+ fingers_okay++;
+
+ return true;
+ });
+
+ return fingers_okay == _attr.fingers;
+ }
+
+ void handle_event(Sink &destination, Input::Event const &ev) override
+ {
+ ev.handle_touch([&] (Input::Touch_id id, float x, float y) {
+
+ Point p {(int)x, (int)y};
+ switch (_state)
+ {
+ case IDLE:
+ if (id.value >= _attr.fingers)
+ return;
+ if (_attr.rect.valid() && !_attr.rect.contains(p))
+ return;
+
+ _state = State::DETECT;
+ _timeout.schedule(_attr.duration);
+ [[fallthrough]];
+ case DETECT:
+ if (id.value >= _attr.fingers) {
+ cancel();
+ return;
+ }
+ _update_finger(id, p);
+
+ if (_detected()) {
+ _state = State::TRIGGERED;
+ _timeout.discard();
+
+ _emit_from_xml(destination, _xml.xml, false);
+ }
+
+ break;
+ case TRIGGERED:
+ /* nothing to be done */
+ break;
+ }
+ });
+
+ if (_state == IDLE)
+ return;
+
+ ev.handle_touch_release([&] (Input::Touch_id id) {
+ if (id.value == 0) {
+ /* emit release events if gesture had been triggered */
+ if (_state == TRIGGERED)
+ _emit_from_xml(destination, _xml.xml, true);
+ cancel();
+ }
+ });
+ }
+
+ void generate(Source::Sink &, Buffer_action & buffer) override
+ {
+ if (_state != State::TRIGGERED)
+ return;
+
+ buffer.clear();
+ }
+
+ Swipe(Registry ®istry,
+ Timer::Connection &timer,
+ Allocator &alloc,
+ Xml_node const &node)
+ : Gesture(registry, alloc, node), _timer(timer),
+ _attr(Attr::from_xml(node))
+ {
+ if (_attr.fingers > MAX_FINGERS)
+ warning("Swipe gesture limited to ", (unsigned)MAX_FINGERS, " fingers");
+ }
+ };
+
+ /*
+ * Hold gesture: Triggers if number of fingers is held for a certain time
+ * - The fingers must stay within a certain area around the first touch.
+ * - If the finger is held after the gesture triggered, touch events
+ * are translated into absolute motion events.
+ */
+ struct Hold : Gesture
+ {
+ Timer::Connection &_timer;
+ Source::Trigger &_trigger;
+
+ Timer::One_shot_timeout _timeout {
+ _timer, *this, &Hold::_handle_timeout };
+
+ struct Attr
+ {
+ Area area;
+ Microseconds delay;
+ unsigned fingers;
+
+ static Microseconds _delay_from_xml(Xml_node const &node)
+ {
+ return Microseconds {
+ node.attribute_value("delay_ms", 1000U)*1000 };
+ }
+
+ static Attr from_xml(Xml_node const &node)
+ {
+ Attr attr {
+ .area = Area::from_xml(node),
+ .delay = _delay_from_xml(node),
+ .fingers = node.attribute_value("fingers", 1U)
+ };
+
+ if (attr.area.w == 0) attr.area.w = 30;
+ if (attr.area.h == 0) attr.area.h = 30;
+
+ return attr;
+ }
+ };
+
+ Attr const _attr;
+
+ /* Rect of size _attr.area around the starting touch */
+ Constructible _rect { };
+
+ unsigned _fingers_present { 0 };
+ Point _last_pos { };
+ bool _emitted { false };
+
+ void _handle_timeout(Duration)
+ {
+ _state = State::TRIGGERED;
+ _emitted = false;
+ _trigger.trigger_generate();
+ }
+
+ void cancel() override
+ {
+ if (_state == State::IDLE)
+ return;
+
+ _timeout.discard();
+ _state = State::IDLE;
+ _fingers_present = 0;
+ }
+
+ void handle_event(Sink &destination, Input::Event const &ev) override
+ {
+ ev.handle_touch([&] (Input::Touch_id id, float x, float y) {
+
+ _fingers_present = max(_fingers_present, id.value+1);
+
+ Point p {(int)x, (int)y};
+ Point diff;
+ switch (_state)
+ {
+ case IDLE:
+ _rect.construct(
+ p - Point { (int)_attr.area.w/2, (int)_attr.area.h/2 },
+ _attr.area
+ );
+ _last_pos = p;
+ _state = State::DETECT;
+ [[fallthrough]];
+ case DETECT:
+ if (_fingers_present > _attr.fingers || !_rect->contains(p)) {
+ cancel();
+ return;
+ }
+ if (!_timeout.scheduled() && _fingers_present == _attr.fingers)
+ _timeout.schedule(_attr.delay);
+ break;
+ case TRIGGERED:
+ /* translate into relative motion events */
+ if (id.value == 0) {
+ diff = p - _last_pos;
+ destination.submit(Input::Relative_motion { diff.x, diff.y });
+ _last_pos = p;
+ }
+ break;
+ }
+ });
+
+ if (_state == IDLE)
+ return;
+
+ ev.handle_touch_release([&] (Input::Touch_id id) {
+ _fingers_present = min(_fingers_present, id.value);
+
+ if (!_fingers_present) {
+ /* emit release events if gesture had been triggered */
+ if (_state == TRIGGERED)
+ _emit_from_xml(destination, _xml.xml, true);
+
+ cancel();
+ }
+ });
+ }
+
+ void generate(Source::Sink &destination, Buffer_action &buffer) override
+ {
+ if (_state != State::TRIGGERED || _emitted)
+ return;
+
+ /* emit absolute motion to trigger focus handling */
+ destination.submit(Input::Absolute_motion { _last_pos.x, _last_pos.y });
+
+ _emit_from_xml(destination, _xml.xml, false);
+ buffer.clear();
+ _emitted = true;
+ }
+
+ Hold(Registry ®istry,
+ Timer::Connection &timer,
+ Source::Trigger &trigger,
+ Allocator &alloc,
+ Xml_node const &node)
+ : Gesture(registry, alloc, node), _timer(timer), _trigger(trigger),
+ _attr(Attr::from_xml(node))
+ { }
+ };
+
+ Registry _gestures { };
+
+ /**
+ * Filter interface
+ */
+ void filter_event(Sink &destination, Input::Event const &event) override
+ {
+ Input::Event ev = event;
+
+ bool active { false };
+ if (ev.touch() || ev.touch_release()) {
+ bool handled { false };
+
+ State old_state = _state;
+ _gestures.for_each([&] (Gesture &gesture) {
+ if (gesture.state() != old_state || handled)
+ return;
+
+ gesture.handle_event(destination, ev);
+
+ if (gesture.state() > _state)
+ _state = gesture.state();
+
+ switch (gesture.state()) {
+ case TRIGGERED:
+ gesture.generate(destination, _buffer);
+ handled = true;
+ [[fallthrough]];
+ case DETECT:
+ active = true;
+ break;
+ case IDLE:
+ break;
+ }
+ });
+
+ /* pass touch events if all gestures were cancelled */
+ if (!active && _state != TRIGGERED) {
+ _buffer.submit(destination);
+ _buffer.clear();
+ destination.submit(ev);
+ }
+
+ } else if (_state != DETECT) {
+ /* pass all non-touch events in IDLE or TRIGGERED */
+ destination.submit(ev);
+ }
+
+ if (active && _state == DETECT) {
+ /* buffer all events if there is any gesture in DETECT */
+ _buffer.store(ev);
+ }
+
+ ev.handle_touch_release([&] (Input::Touch_id id) {
+ /* cancel all gestures if all fingers have been released */
+ if (id.value == 0) {
+ _state = State::IDLE;
+
+ _gestures.for_each([&] (Gesture & gesture) {
+ gesture.cancel(); });
+ }
+ });
+
+ }
+
+ public:
+
+ static char const *name() { return "touch-gesture"; }
+
+ Touch_gesture_source(Owner &owner, Xml_node const &config,
+ Source::Factory &factory,
+ Timer_accessor &timer_accessor,
+ Source::Trigger &trigger,
+ Allocator &alloc)
+ :
+ Source(owner),
+ _owner(factory),
+ _source(factory.create_source_for_sub_node(_owner, config)),
+ _alloc(alloc)
+ {
+ config.for_each_sub_node("hold", [&] (Xml_node const &node) {
+ new (_alloc) Hold(_gestures, timer_accessor.timer(), trigger, _alloc, node); });
+
+ config.for_each_sub_node("swipe", [&] (Xml_node const &node) {
+ new (_alloc) Swipe(_gestures, timer_accessor.timer(), _alloc, node); });
+ }
+
+ ~Touch_gesture_source()
+ {
+ _gestures.for_each([&] (Gesture &gesture) {
+ destroy(_alloc, &gesture); });
+ }
+
+ void generate(Sink &destination) override
+ {
+ if (_state == DETECT) {
+ bool handled { false };
+ _gestures.for_each([&] (Gesture & gesture) {
+ if (handled || gesture.state() != TRIGGERED)
+ return;
+
+ gesture.generate(destination, _buffer);
+ _state = TRIGGERED;
+ handled = true;
+ });
+ }
+
+ Source::Filter::apply(destination, *this, _source);
+ }
+};
+
+#endif /* _EVENT_FILTER__TOUCH_GESTURE_SOURCE_H_*/
diff --git a/repos/os/src/test/event_filter/main.cc b/repos/os/src/test/event_filter/main.cc
index 795e931e4c..58d71df300 100644
--- a/repos/os/src/test/event_filter/main.cc
+++ b/repos/os/src/test/event_filter/main.cc
@@ -261,11 +261,12 @@ class Test::Input_to_filter
(int)node.attribute_value("ry", 0L)});
if (node.has_type("touch"))
- batch.submit(Input::Touch{ { 0 }, (float)node.attribute_value("x", 0.0),
- (float)node.attribute_value("y", 0.0)});
+ batch.submit(Input::Touch{ { node.attribute_value("id", 0U) },
+ (float)node.attribute_value("x", 0.0),
+ (float)node.attribute_value("y", 0.0)});
if (node.has_type("touch-release"))
- batch.submit(Input::Touch_release { { 0 } } );
+ batch.submit(Input::Touch_release { { node.attribute_value("id", 0U) } } );
});
});
}
@@ -514,15 +515,17 @@ struct Test::Main : Input_from_filter::Event_handler
&& (!step.has_attribute("ay") || step.attribute_value("ay", 0L) == y))
step_succeeded = true; });
- ev.handle_touch([&] (Input::Touch_id, float x, float y) {
+ ev.handle_touch([&] (Input::Touch_id id, float x, float y) {
if (step.type() == "expect_touch"
&& ((float)step.attribute_value("x", 0.0) == x)
- && ((float)step.attribute_value("y", 0.0) == y))
+ && ((float)step.attribute_value("y", 0.0) == y)
+ && (step.attribute_value("id", 0U) == id.value))
step_succeeded = true;
});
- ev.handle_touch_release([&] (Input::Touch_id) {
- if (step.type() == "expect_touch_release")
+ ev.handle_touch_release([&] (Input::Touch_id id) {
+ if (step.type() == "expect_touch_release"
+ && (step.attribute_value("id", 0U) == id.value))
step_succeeded = true;
});