diff --git a/repos/gems/recipes/pkg/screenshot_trigger/README b/repos/gems/recipes/pkg/screenshot_trigger/README
new file mode 100644
index 0000000000..3f9fae42c5
--- /dev/null
+++ b/repos/gems/recipes/pkg/screenshot_trigger/README
@@ -0,0 +1,2 @@
+
+ Virtual print button for a touch-screen device
diff --git a/repos/gems/recipes/pkg/screenshot_trigger/archives b/repos/gems/recipes/pkg/screenshot_trigger/archives
new file mode 100644
index 0000000000..bcf70a4ae6
--- /dev/null
+++ b/repos/gems/recipes/pkg/screenshot_trigger/archives
@@ -0,0 +1 @@
+_/src/screenshot_trigger
diff --git a/repos/gems/recipes/pkg/screenshot_trigger/hash b/repos/gems/recipes/pkg/screenshot_trigger/hash
new file mode 100644
index 0000000000..9fc0713931
--- /dev/null
+++ b/repos/gems/recipes/pkg/screenshot_trigger/hash
@@ -0,0 +1 @@
+2023-01-29 a87e1719fd98401958f47ff3ae9f0c641b4c6094
diff --git a/repos/gems/recipes/pkg/screenshot_trigger/runtime b/repos/gems/recipes/pkg/screenshot_trigger/runtime
new file mode 100644
index 0000000000..e57ee6334f
--- /dev/null
+++ b/repos/gems/recipes/pkg/screenshot_trigger/runtime
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/repos/gems/recipes/src/screenshot_trigger/content.mk b/repos/gems/recipes/src/screenshot_trigger/content.mk
new file mode 100644
index 0000000000..06909396ad
--- /dev/null
+++ b/repos/gems/recipes/src/screenshot_trigger/content.mk
@@ -0,0 +1,2 @@
+SRC_DIR = src/app/screenshot_trigger
+include $(GENODE_DIR)/repos/base/recipes/src/content.inc
diff --git a/repos/gems/recipes/src/screenshot_trigger/hash b/repos/gems/recipes/src/screenshot_trigger/hash
new file mode 100644
index 0000000000..f2470cffe5
--- /dev/null
+++ b/repos/gems/recipes/src/screenshot_trigger/hash
@@ -0,0 +1 @@
+2023-01-29-a a4f24340626a9eb891f8de5061d2e1bb38ba56dc
diff --git a/repos/gems/recipes/src/screenshot_trigger/used_apis b/repos/gems/recipes/src/screenshot_trigger/used_apis
new file mode 100644
index 0000000000..6e9b9cd67c
--- /dev/null
+++ b/repos/gems/recipes/src/screenshot_trigger/used_apis
@@ -0,0 +1,10 @@
+base
+os
+blit
+gems
+framebuffer_session
+input_session
+gui_session
+event_session
+timer_session
+nitpicker_gfx
diff --git a/repos/gems/run/screenshot_trigger.run b/repos/gems/run/screenshot_trigger.run
new file mode 100644
index 0000000000..3c1004d7e1
--- /dev/null
+++ b/repos/gems/run/screenshot_trigger.run
@@ -0,0 +1,94 @@
+create_boot_directory
+
+import_from_depot [depot_user]/src/[base_src] \
+ [depot_user]/pkg/[drivers_interactive_pkg] \
+ [depot_user]/src/report_rom \
+ [depot_user]/src/nitpicker \
+ [depot_user]/src/init
+
+install_config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+set fd [open [run_dir]/genode/focus w]
+puts $fd " \"/>"
+close $fd
+
+build { app/screenshot_trigger }
+
+build_boot_image [build_artifacts]
+
+run_genode_until forever
diff --git a/repos/gems/sculpt/launcher/screenshot_trigger b/repos/gems/sculpt/launcher/screenshot_trigger
new file mode 100644
index 0000000000..e1ce470041
--- /dev/null
+++ b/repos/gems/sculpt/launcher/screenshot_trigger
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/repos/gems/src/app/screenshot_trigger/main.cc b/repos/gems/src/app/screenshot_trigger/main.cc
new file mode 100644
index 0000000000..5bd6b8b3c0
--- /dev/null
+++ b/repos/gems/src/app/screenshot_trigger/main.cc
@@ -0,0 +1,178 @@
+/*
+ * \brief Virtual print button
+ * \author Norman Feske
+ * \date 2023-01-29
+ */
+
+/*
+ * Copyright (C) 2023 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.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Screenshot_trigger {
+ using namespace Genode;
+ struct Main;
+}
+
+
+struct Screenshot_trigger::Main
+{
+ Env &_env;
+
+ using Point = Gui_buffer::Point;
+ using Area = Gui_buffer::Area;
+ using Rect = Gui_buffer::Rect;
+
+ unsigned _size { };
+ Point _position { };
+ Area _area { };
+
+ Color const _color { 200, 0, 0 };
+
+ Input::Keycode const _keycode = Input::KEY_PRINT;
+
+ uint64_t const _timeout_us = 1*1000*1000;
+
+ Gui ::Connection _gui { _env };
+ Event::Connection _event { _env };
+ Timer::Connection _timer { _env };
+
+ Constructible _gui_buffer { };
+
+ struct View
+ {
+ Gui::Connection &_gui;
+
+ Gui::Session::View_handle _handle { _gui.create_view() };
+
+ View(Gui::Connection &gui, Point position, Area size) : _gui(gui)
+ {
+ using Command = Gui::Session::Command;
+ _gui.enqueue(_handle, Rect(position, size));
+ _gui.enqueue(_handle, Gui::Session::View_handle());
+ _gui.execute();
+ }
+
+ ~View() { _gui.destroy_view(_handle); }
+ };
+
+ Constructible _view { };
+
+ Signal_handler _timer_handler { _env.ep(), *this, &Main::_handle_timer };
+ Signal_handler _input_handler { _env.ep(), *this, &Main::_handle_input };
+
+ /* used for hiding the view for a second after triggering */
+ bool _visible = true;
+
+ void visible(bool visible)
+ {
+ _visible = visible;
+ _view.conditional(visible, _gui, _position, _area);
+ }
+
+ void _handle_input()
+ {
+ _gui.input()->for_each_event([&] (Input::Event const &ev) {
+
+ if (!_visible) /* ignore events while the view is invisble */
+ return;
+
+ bool const triggered = ev.key_release(Input::BTN_LEFT)
+ || ev.touch_release();
+ if (!triggered)
+ return;
+
+ /* hide trigger for some time */
+ visible(false);
+ _timer.trigger_once(_timeout_us);
+
+ /* generate synthetic key-press-release sequence */
+ _event.with_batch([&] (Event::Connection::Batch &batch) {
+ batch.submit(Input::Press { _keycode });
+ batch.submit(Input::Release { _keycode });
+ });
+ });
+ }
+
+ void _handle_timer()
+ {
+ if (!_visible)
+ visible(true);
+ }
+
+ void _render(Gui_buffer::Pixel_surface &pixel, Gui_buffer::Alpha_surface &alpha)
+ {
+ Box_painter::paint(pixel, Rect(Point(0, 0), _area), _color);
+
+ long const half = _size/2;
+ long const max_sq = half*half;
+
+ auto intensity = [&] (long x, long y)
+ {
+ x -= half,
+ y -= half;
+
+ long const r_sq = x*x + y*y;
+
+ return 255 - min(255l, (r_sq*255)/max_sq);
+ };
+
+ /* fill alpha channel */
+ Pixel_alpha8 *base = alpha.addr();
+ for (unsigned y = 0; y < _area.h(); y++)
+ for (unsigned x = 0; x < _area.w(); x++)
+ *base++ = Pixel_alpha8 { 0, 0, 0, int(intensity(x, y)) };
+ }
+
+ Attached_rom_dataspace _config { _env, "config" };
+
+ Signal_handler _config_handler { _env.ep(), *this, &Main::_handle_config };
+
+ void _handle_config()
+ {
+ _config.update();
+
+ Xml_node const config = _config.xml();
+
+ _size = config.attribute_value("size", 50u);
+ _position = Point::from_xml(config);
+ _area = Area(_size, _size);
+
+ _gui_buffer.construct(_gui, _area, _env.ram(), _env.rm());
+
+ _gui_buffer->apply_to_surface([&] (auto &pixel, auto &alpha) {
+ _render(pixel, alpha); });
+
+ _gui_buffer->flush_surface();
+ }
+
+ Main(Env &env) : _env(env)
+ {
+ _config.sigh(_config_handler);
+ _handle_config();
+
+ _gui.input()->sigh(_input_handler);
+ _timer.sigh(_timer_handler);
+
+ visible(true);
+ }
+};
+
+
+void Component::construct(Genode::Env &env)
+{
+ static Screenshot_trigger::Main main(env);
+}
diff --git a/repos/gems/src/app/screenshot_trigger/target.mk b/repos/gems/src/app/screenshot_trigger/target.mk
new file mode 100644
index 0000000000..18793b1204
--- /dev/null
+++ b/repos/gems/src/app/screenshot_trigger/target.mk
@@ -0,0 +1,3 @@
+TARGET = screenshot_trigger
+SRC_CC = main.cc
+LIBS = base blit