diff --git a/repos/os/src/server/event_filter/README b/repos/os/src/server/event_filter/README
index 43261612d9..134c95d1af 100644
--- a/repos/os/src/server/event_filter/README
+++ b/repos/os/src/server/event_filter/README
@@ -79,6 +79,13 @@ one of the following filters:
   "0" corresponds to a linear function whereas the maximum value "255" applies
   a curved function. The default value is "127".
 
+:<touch-click>:
+
+  Augments touch events with artificial absolute motion and mouse click/clack
+  events as understood by regular GUI applications that are not aware of
+  touch input. The original touch events are preserved, enabling touch-aware
+  applications to interpet touch gestures.
+
 
 Character generator rules
 -------------------------
diff --git a/repos/os/src/server/event_filter/main.cc b/repos/os/src/server/event_filter/main.cc
index 56c07fa1e6..e2fbcfda79 100644
--- a/repos/os/src/server/event_filter/main.cc
+++ b/repos/os/src/server/event_filter/main.cc
@@ -25,6 +25,7 @@
 #include <button_scroll_source.h>
 #include <accelerate_source.h>
 #include <log_source.h>
+#include <touch_click_source.h>
 #include <event_session.h>
 
 namespace Event_filter { struct Main; }
@@ -250,6 +251,9 @@ struct Event_filter::Main : Source::Factory, Source::Trigger
 		if (node.type() == Log_source::name())
 			return *new (_heap) Log_source(owner, node, *this);
 
+		if (node.type() == Touch_click_source::name())
+			return *new (_heap) Touch_click_source(owner, node, *this);
+
 		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 d45176bddc..629ff0cc89 100644
--- a/repos/os/src/server/event_filter/source.h
+++ b/repos/os/src/server/event_filter/source.h
@@ -47,7 +47,8 @@ class Event_filter::Source
 			    || node.type() == "merge"
 			    || node.type() == "button-scroll"
 			    || node.type() == "accelerate"
-			    || node.type() == "log";
+			    || node.type() == "log"
+			    || node.type() == "touch-click";
 
 			return false;
 		}
diff --git a/repos/os/src/server/event_filter/touch_click_source.h b/repos/os/src/server/event_filter/touch_click_source.h
new file mode 100644
index 0000000000..cc84a12f82
--- /dev/null
+++ b/repos/os/src/server/event_filter/touch_click_source.h
@@ -0,0 +1,96 @@
+/*
+ * \brief  Input-event source that augments touch events with pointer events
+ * \author Norman Feske
+ * \date   2021-11-22
+ *
+ * This filter supplements touch events with absolute motion events and
+ * artificial mouse click/release events as understood by regular GUI
+ * applications. The original touch events are preserved, which enables
+ * touch-aware applications to interpret them.
+ */
+
+/*
+ * Copyright (C) 2021 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_CLICK_SOURCE_H_
+#define _EVENT_FILTER__TOUCH_CLICK_SOURCE_H_
+
+/* Genode includes */
+#include <input/keycodes.h>
+
+/* local includes */
+#include <source.h>
+
+namespace Event_filter { class Touch_click_source; }
+
+
+class Event_filter::Touch_click_source : public Source, Source::Filter
+{
+	private:
+
+		Owner _owner;
+
+		Source &_source;
+
+		bool _pressed = false;
+
+		/**
+		 * Filter interface
+		 */
+		void filter_event(Sink &destination, Input::Event const &event) override
+		{
+			Input::Event ev = event;
+
+			/* forward original event */
+			destination.submit(ev);
+
+			/* supplement mouse click and absolute motion */
+			ev.handle_touch([&] (Input::Touch_id id, float x, float y) {
+
+				/* respond to first finger only */
+				if (id.value != 0)
+					return;
+
+				destination.submit(Input::Absolute_motion{ int(x), int(y) });
+
+				if (!_pressed) {
+					destination.submit(Input::Press { Input::BTN_LEFT });
+					_pressed = true;
+				}
+			});
+
+			/* supplement mouse clack */
+			ev.handle_touch_release([&] (Input::Touch_id id) {
+
+				if (id.value != 0)
+					return;
+
+				if (_pressed) {
+					destination.submit(Input::Release { Input::BTN_LEFT });
+					_pressed = false;
+				}
+			});
+		}
+
+	public:
+
+		static char const *name() { return "touch-click"; }
+
+		Touch_click_source(Owner &owner, Xml_node config, Source::Factory &factory)
+		:
+			Source(owner),
+			_owner(factory),
+			_source(factory.create_source(_owner, input_sub_node(config)))
+		{ }
+
+		void generate(Sink &destination) override
+		{
+			Source::Filter::apply(destination, *this, _source);
+		}
+};
+
+#endif /* _EVENT_FILTER__TOUCH_CLICK_SOURCE_H_*/