diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7cbc5b61..87a35bdd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -55,8 +55,10 @@
release, to foot.
* Support for the new `cursor-shape-v1` Wayland protocol, i.e. server
side cursor shapes ([#1379][1379]).
+* Support for touchscreen input ([#517][517]).
[1379]: https://codeberg.org/dnkl/foot/issues/1379
+[517]: https://codeberg.org/dnkl/foot/issues/517
### Changed
diff --git a/README.md b/README.md
index 0d6262dc..42be5792 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@ The fast, lightweight and minimalistic Wayland terminal emulator.
1. [Normal mode](#normal-mode)
1. [Scrollback search](#scrollback-search)
1. [Mouse](#mouse)
+ 1. [Touchscreen](#touchscreen)
1. [Server (daemon) mode](#server-daemon-mode)
1. [URLs](#urls)
1. [Shell integration](#shell-integration)
@@ -246,6 +247,17 @@ These are the default shortcuts. See `man foot.ini` and the example
: Scroll up/down in history
+### Touchscreen
+
+tap
+: Emulates mouse left button click.
+
+drag
+: Scrolls up/down in history.
+: Holding for a while before dragging (time delay can be configured)
+ emulates mouse dragging with left button held.
+
+
## Server (daemon) mode
When run normally, **foot** is a single-window application; if you
diff --git a/config.c b/config.c
index 3d02355f..5297bbdc 100644
--- a/config.c
+++ b/config.c
@@ -2475,6 +2475,20 @@ parse_section_tweak(struct context *ctx)
}
}
+static bool
+parse_section_touch(struct context *ctx) {
+ struct config *conf = ctx->conf;
+ const char *key = ctx->key;
+
+ if (strcmp(key, "long-press-delay") == 0)
+ return value_to_uint32(ctx, 10, &conf->touch.long_press_delay);
+
+ else {
+ LOG_CONTEXTUAL_ERR("not a valid option: %s", key);
+ return false;
+ }
+}
+
static bool
parse_key_value(char *kv, const char **section, const char **key, const char **value)
{
@@ -2554,6 +2568,7 @@ enum section {
SECTION_TEXT_BINDINGS,
SECTION_ENVIRONMENT,
SECTION_TWEAK,
+ SECTION_TOUCH,
SECTION_COUNT,
};
@@ -2579,6 +2594,7 @@ static const struct {
[SECTION_TEXT_BINDINGS] = {&parse_section_text_bindings, "text-bindings"},
[SECTION_ENVIRONMENT] = {&parse_section_environment, "environment"},
[SECTION_TWEAK] = {&parse_section_tweak, "tweak"},
+ [SECTION_TOUCH] = {&parse_section_touch, "touch"},
};
static_assert(ALEN(section_info) == SECTION_COUNT, "section info array size mismatch");
@@ -3026,6 +3042,10 @@ config_load(struct config *conf, const char *conf_path,
.sixel = true,
},
+ .touch = {
+ .long_press_delay = 400,
+ },
+
.env_vars = tll_init(),
#if defined(UTMP_DEFAULT_HELPER_PATH)
.utmp_helper_path = ((strlen(UTMP_DEFAULT_HELPER_PATH) > 0 &&
diff --git a/config.h b/config.h
index 2034752f..20c07f6c 100644
--- a/config.h
+++ b/config.h
@@ -347,6 +347,10 @@ struct config {
bool sixel;
} tweak;
+ struct {
+ uint32_t long_press_delay;
+ } touch;
+
user_notifications_t notifications;
};
diff --git a/doc/foot.1.scd b/doc/foot.1.scd
index 60420bef..1cdf47e4 100644
--- a/doc/foot.1.scd
+++ b/doc/foot.1.scd
@@ -283,6 +283,18 @@ default) available; see *foot.ini*(5).
*wheel*
Scroll up/down in history
+## TOUCHSCREEN
+
+*tap*
+ Emulates mouse left button click.
+
+*drag*
+ Scrolls up/down in history.
+
+ Holding for a while before dragging (time delay can be configured)
+ emulates mouse dragging with left button held.
+
+
# FONT FORMAT
The font is specified in FontConfig syntax. That is, a colon-separated
diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd
index e28cf416..ac22ae5a 100644
--- a/doc/foot.ini.5.scd
+++ b/doc/foot.ini.5.scd
@@ -535,6 +535,14 @@ applications can change these at runtime.
Default: _yes_.
+# SECTION: touch
+
+*long-press-delay*
+ Number of milliseconds to distinguish between a short press and
+ a long press on the touchscreen.
+
+ Default: _400_.
+
# SECTION: colors
This section controls the 16 ANSI colors, the default foreground and
diff --git a/foot.ini b/foot.ini
index 61e88aec..94d82f6f 100644
--- a/foot.ini
+++ b/foot.ini
@@ -70,6 +70,9 @@
# hide-when-typing=no
# alternate-scroll-mode=yes
+[touch]
+# long-press-delay=400
+
[colors]
# alpha=1.0
# background=002b36
diff --git a/input.c b/input.c
index 0f638cc1..3bf6535a 100644
--- a/input.c
+++ b/input.c
@@ -1721,6 +1721,36 @@ xcursor_for_csd_border(struct terminal *term, int x, int y)
}
}
+static void
+mouse_button_state_reset(struct seat *seat)
+{
+ tll_free(seat->mouse.buttons);
+ seat->mouse.count = 0;
+ seat->mouse.last_released_button = 0;
+ memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time));
+}
+
+static void
+mouse_coord_pixel_to_cell(struct seat *seat, const struct terminal *term,
+ int x, int y)
+{
+ /*
+ * Translate x,y pixel coordinate to a cell coordinate, or -1
+ * if the cursor is outside the grid. I.e. if it is inside the
+ * margins.
+ */
+
+ if (x < term->margins.left || x >= term->width - term->margins.right)
+ seat->mouse.col = -1;
+ else
+ seat->mouse.col = (x - term->margins.left) / term->cell_width;
+
+ if (y < term->margins.top || y >= term->height - term->margins.bottom)
+ seat->mouse.row = -1;
+ else
+ seat->mouse.row = (y - term->margins.top) / term->cell_height;
+}
+
static void
wl_pointer_enter(void *data, struct wl_pointer *wl_pointer,
uint32_t serial, struct wl_surface *surface,
@@ -1733,6 +1763,24 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer,
}
struct seat *seat = data;
+
+ if (seat->wl_touch != NULL) {
+ switch (seat->touch.state) {
+ case TOUCH_STATE_IDLE:
+ mouse_button_state_reset(seat);
+ seat->touch.state = TOUCH_STATE_INHIBITED;
+ break;
+
+ case TOUCH_STATE_INHIBITED:
+ break;
+
+ case TOUCH_STATE_HELD:
+ case TOUCH_STATE_DRAGGING:
+ case TOUCH_STATE_SCROLLING:
+ return;
+ }
+ }
+
struct wl_window *win = wl_surface_get_user_data(surface);
struct terminal *term = win->term;
@@ -1759,22 +1807,7 @@ wl_pointer_enter(void *data, struct wl_pointer *wl_pointer,
switch (term->active_surface) {
case TERM_SURF_GRID: {
- /*
- * Translate x,y pixel coordinate to a cell coordinate, or -1
- * if the cursor is outside the grid. I.e. if it is inside the
- * margins.
- */
-
- if (x < term->margins.left || x >= term->width - term->margins.right)
- seat->mouse.col = -1;
- else
- seat->mouse.col = (x - term->margins.left) / term->cell_width;
-
- if (y < term->margins.top || y >= term->height - term->margins.bottom)
- seat->mouse.row = -1;
- else
- seat->mouse.row = (y - term->margins.top) / term->cell_height;
-
+ mouse_coord_pixel_to_cell(seat, term, x, y);
break;
}
@@ -1802,6 +1835,14 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer,
uint32_t serial, struct wl_surface *surface)
{
struct seat *seat = data;
+
+ if (seat->wl_touch != NULL) {
+ if (seat->touch.state != TOUCH_STATE_INHIBITED) {
+ return;
+ }
+ seat->touch.state = TOUCH_STATE_IDLE;
+ }
+
struct terminal *old_moused = seat->mouse_focus;
LOG_DBG(
@@ -1824,10 +1865,7 @@ wl_pointer_leave(void *data, struct wl_pointer *wl_pointer,
/* Reset mouse state */
seat->mouse.x = seat->mouse.y = 0;
seat->mouse.col = seat->mouse.row = 0;
- tll_free(seat->mouse.buttons);
- seat->mouse.count = 0;
- seat->mouse.last_released_button = 0;
- memset(&seat->mouse.last_time, 0, sizeof(seat->mouse.last_time));
+ mouse_button_state_reset(seat);
for (size_t i = 0; i < ALEN(seat->mouse.aggregated); i++)
seat->mouse.aggregated[i] = 0.0;
seat->mouse.have_discrete = false;
@@ -1879,6 +1917,11 @@ wl_pointer_motion(void *data, struct wl_pointer *wl_pointer,
uint32_t time, wl_fixed_t surface_x, wl_fixed_t surface_y)
{
struct seat *seat = data;
+
+ /* Touch-emulated pointer events have wl_pointer == NULL. */
+ if (wl_pointer != NULL && seat->touch.state != TOUCH_STATE_INHIBITED)
+ return;
+
struct wayland *wayl = seat->wayl;
struct terminal *term = seat->mouse_focus;
@@ -2102,6 +2145,11 @@ wl_pointer_button(void *data, struct wl_pointer *wl_pointer,
xassert(serial != 0);
struct seat *seat = data;
+
+ /* Touch-emulated pointer events have wl_pointer == NULL. */
+ if (wl_pointer != NULL && seat->touch.state != TOUCH_STATE_INHIBITED)
+ return;
+
struct wayland *wayl = seat->wayl;
struct terminal *term = seat->mouse_focus;
@@ -2559,6 +2607,9 @@ wl_pointer_axis(void *data, struct wl_pointer *wl_pointer,
{
struct seat *seat = data;
+ if (seat->touch.state != TOUCH_STATE_INHIBITED)
+ return;
+
if (seat->mouse.have_discrete)
return;
@@ -2588,6 +2639,10 @@ wl_pointer_axis_discrete(void *data, struct wl_pointer *wl_pointer,
uint32_t axis, int32_t discrete)
{
struct seat *seat = data;
+
+ if (seat->touch.state != TOUCH_STATE_INHIBITED)
+ return;
+
seat->mouse.have_discrete = true;
int amount = discrete;
@@ -2604,6 +2659,10 @@ static void
wl_pointer_frame(void *data, struct wl_pointer *wl_pointer)
{
struct seat *seat = data;
+
+ if (seat->touch.state != TOUCH_STATE_INHIBITED)
+ return;
+
seat->mouse.have_discrete = false;
}
@@ -2619,6 +2678,9 @@ wl_pointer_axis_stop(void *data, struct wl_pointer *wl_pointer,
{
struct seat *seat = data;
+ if (seat->touch.state != TOUCH_STATE_INHIBITED)
+ return;
+
xassert(axis < ALEN(seat->mouse.aggregated));
seat->mouse.aggregated[axis] = 0.;
}
@@ -2634,3 +2696,167 @@ const struct wl_pointer_listener pointer_listener = {
.axis_stop = wl_pointer_axis_stop,
.axis_discrete = wl_pointer_axis_discrete,
};
+
+static bool
+touch_to_scroll(struct seat *seat, struct terminal *term,
+ wl_fixed_t surface_x, wl_fixed_t surface_y)
+{
+ bool coord_updated = false;
+
+ int y = wl_fixed_to_int(surface_y) * term->scale;
+ int rows = (y - seat->mouse.y) / term->cell_height;
+ if (rows != 0) {
+ mouse_scroll(seat, -rows, WL_POINTER_AXIS_VERTICAL_SCROLL);
+ seat->mouse.y += rows * term->cell_height;
+ coord_updated = true;
+ }
+
+ int x = wl_fixed_to_int(surface_x) * term->scale;
+ int cols = (x - seat->mouse.x) / term->cell_width;
+ if (cols != 0) {
+ mouse_scroll(seat, -cols, WL_POINTER_AXIS_HORIZONTAL_SCROLL);
+ seat->mouse.x += cols * term->cell_width;
+ coord_updated = true;
+ }
+
+ return coord_updated;
+}
+
+static void
+wl_touch_down(void *data, struct wl_touch *wl_touch, uint32_t serial,
+ uint32_t time, struct wl_surface *surface, int32_t id,
+ wl_fixed_t surface_x, wl_fixed_t surface_y)
+{
+ struct seat *seat = data;
+
+ if (seat->touch.state != TOUCH_STATE_IDLE)
+ return;
+
+ struct wl_window *win = wl_surface_get_user_data(surface);
+ struct terminal *term = win->term;
+
+ term->active_surface = term_surface_kind(term, surface);
+ if (term->active_surface != TERM_SURF_GRID)
+ return;
+
+ LOG_DBG("touch_down: touch=%p, x=%d, y=%d", (void *)wl_touch,
+ wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y));
+
+ int x = wl_fixed_to_int(surface_x) * term->scale;
+ int y = wl_fixed_to_int(surface_y) * term->scale;
+
+ seat->mouse.x = x;
+ seat->mouse.y = y;
+ mouse_coord_pixel_to_cell(seat, term, x, y);
+
+ seat->touch.state = TOUCH_STATE_HELD;
+ seat->touch.serial = serial;
+ seat->touch.time = time + term->conf->touch.long_press_delay;
+ seat->touch.surface = surface;
+ seat->touch.id = id;
+}
+
+static void
+wl_touch_up(void *data, struct wl_touch *wl_touch, uint32_t serial,
+ uint32_t time, int32_t id)
+{
+ struct seat *seat = data;
+
+ if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id)
+ return;
+
+ LOG_DBG("touch_up: touch=%p", (void *)wl_touch);
+
+ struct wl_window *win = wl_surface_get_user_data(seat->touch.surface);
+ struct terminal *term = win->term;
+
+ seat->mouse_focus = term;
+
+ switch (seat->touch.state) {
+ case TOUCH_STATE_HELD:
+ wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT,
+ WL_POINTER_BUTTON_STATE_PRESSED);
+ /* fallthrough */
+ case TOUCH_STATE_DRAGGING:
+ wl_pointer_button(seat, NULL, serial, time, BTN_LEFT,
+ WL_POINTER_BUTTON_STATE_RELEASED);
+ /* fallthrough */
+ case TOUCH_STATE_SCROLLING:
+ seat->touch.state = TOUCH_STATE_IDLE;
+ break;
+
+ case TOUCH_STATE_INHIBITED:
+ case TOUCH_STATE_IDLE:
+ BUG("Bad touch state: %d", seat->touch.state);
+ break;
+ }
+
+ seat->mouse_focus = NULL;
+}
+
+static void
+wl_touch_motion(void *data, struct wl_touch *wl_touch, uint32_t time,
+ int32_t id, wl_fixed_t surface_x, wl_fixed_t surface_y)
+{
+ struct seat *seat = data;
+ if (seat->touch.state <= TOUCH_STATE_IDLE || id != seat->touch.id)
+ return;
+
+ LOG_DBG("touch_motion: touch=%p, x=%d, y=%d", (void *)wl_touch,
+ wl_fixed_to_int(surface_x), wl_fixed_to_int(surface_y));
+
+ struct wl_window *win = wl_surface_get_user_data(seat->touch.surface);
+ struct terminal *term = win->term;
+
+ seat->mouse_focus = term;
+
+ switch (seat->touch.state) {
+ case TOUCH_STATE_HELD:
+ if (time <= seat->touch.time) {
+ if (touch_to_scroll(seat, term, surface_x, surface_y))
+ seat->touch.state = TOUCH_STATE_SCROLLING;
+ break;
+ } else {
+ wl_pointer_button(seat, NULL, seat->touch.serial, time, BTN_LEFT,
+ WL_POINTER_BUTTON_STATE_PRESSED);
+ seat->touch.state = TOUCH_STATE_DRAGGING;
+ /* fallthrough */
+ }
+ case TOUCH_STATE_DRAGGING:
+ wl_pointer_motion(seat, NULL, time, surface_x, surface_y);
+ break;
+ case TOUCH_STATE_SCROLLING:
+ touch_to_scroll(seat, term, surface_x, surface_y);
+ break;
+
+ case TOUCH_STATE_INHIBITED:
+ case TOUCH_STATE_IDLE:
+ BUG("Bad touch state: %d", seat->touch.state);
+ break;
+ }
+
+ seat->mouse_focus = NULL;
+}
+
+static void
+wl_touch_frame(void *data, struct wl_touch *wl_touch)
+{
+}
+
+static void
+wl_touch_cancel(void *data, struct wl_touch *wl_touch)
+{
+ struct seat *seat = data;
+ if (seat->touch.state == TOUCH_STATE_INHIBITED)
+ return;
+
+ seat->touch.state = TOUCH_STATE_IDLE;
+}
+
+const struct wl_touch_listener touch_listener = {
+ .down = wl_touch_down,
+ .up = wl_touch_up,
+ .motion = wl_touch_motion,
+ .frame = wl_touch_frame,
+ .cancel = wl_touch_cancel,
+};
diff --git a/input.h b/input.h
index 825dc3be..906008d5 100644
--- a/input.h
+++ b/input.h
@@ -26,6 +26,7 @@
extern const struct wl_keyboard_listener keyboard_listener;
extern const struct wl_pointer_listener pointer_listener;
+extern const struct wl_touch_listener touch_listener;
void input_repeat(struct seat *seat, uint32_t key);
diff --git a/tests/test-config.c b/tests/test-config.c
index c70f7a43..e59c104e 100644
--- a/tests/test-config.c
+++ b/tests/test-config.c
@@ -662,6 +662,21 @@ test_section_mouse(void)
config_free(&conf);
}
+static void
+test_section_touch(void)
+{
+ struct config conf = {0};
+ struct context ctx = {
+ .conf = &conf, .section = "touch", .path = "unittest"};
+
+ test_invalid_key(&ctx, &parse_section_touch, "invalid-key");
+
+ test_uint32(&ctx, &parse_section_touch, "long-press-delay",
+ &conf.touch.long_press_delay);
+
+ config_free(&conf);
+}
+
static void
test_section_colors(void)
{
@@ -1347,6 +1362,7 @@ main(int argc, const char *const *argv)
test_section_url();
test_section_cursor();
test_section_mouse();
+ test_section_touch();
test_section_colors();
test_section_csd();
test_section_key_bindings();
diff --git a/wayland.c b/wayland.c
index e862b5e8..a25ec0f6 100644
--- a/wayland.c
+++ b/wayland.c
@@ -222,6 +222,8 @@ seat_destroy(struct seat *seat)
wl_keyboard_release(seat->wl_keyboard);
if (seat->wl_pointer != NULL)
wl_pointer_release(seat->wl_pointer);
+ if (seat->wl_touch != NULL)
+ wl_touch_release(seat->wl_touch);
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
if (seat->wl_text_input != NULL)
@@ -284,9 +286,10 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat,
struct seat *seat = data;
xassert(seat->wl_seat == wl_seat);
- LOG_DBG("%s: keyboard=%s, pointer=%s", seat->name,
+ LOG_DBG("%s: keyboard=%s, pointer=%s, touch=%s", seat->name,
(caps & WL_SEAT_CAPABILITY_KEYBOARD) ? "yes" : "no",
- (caps & WL_SEAT_CAPABILITY_POINTER) ? "yes" : "no");
+ (caps & WL_SEAT_CAPABILITY_POINTER) ? "yes" : "no",
+ (caps & WL_SEAT_CAPABILITY_TOUCH) ? "yes" : "no");
if (caps & WL_SEAT_CAPABILITY_KEYBOARD) {
if (seat->wl_keyboard == NULL) {
@@ -359,6 +362,22 @@ seat_handle_capabilities(void *data, struct wl_seat *wl_seat,
seat->pointer.cursor = NULL;
}
}
+
+ if (caps & WL_SEAT_CAPABILITY_TOUCH) {
+ if (seat->wl_touch == NULL) {
+ seat->wl_touch = wl_seat_get_touch(wl_seat);
+ wl_touch_add_listener(seat->wl_touch, &touch_listener, seat);
+
+ seat->touch.state = TOUCH_STATE_IDLE;
+ }
+ } else {
+ if (seat->wl_touch != NULL) {
+ wl_touch_release(seat->wl_touch);
+ seat->wl_touch = NULL;
+ }
+
+ seat->touch.state = TOUCH_STATE_INHIBITED;
+ }
}
static void
diff --git a/wayland.h b/wayland.h
index d2c1ead1..6d1cd727 100644
--- a/wayland.h
+++ b/wayland.h
@@ -47,6 +47,14 @@ enum data_offer_mime_type {
DATA_OFFER_MIME_TEXT_UTF8_STRING,
};
+enum touch_state {
+ TOUCH_STATE_INHIBITED = -1,
+ TOUCH_STATE_IDLE,
+ TOUCH_STATE_HELD,
+ TOUCH_STATE_DRAGGING,
+ TOUCH_STATE_SCROLLING,
+};
+
struct wayl_surface {
struct wl_surface *surf;
#if defined(HAVE_FRACTIONAL_SCALE)
@@ -165,6 +173,17 @@ struct seat {
bool xcursor_pending;
} pointer;
+ /* Touch state */
+ struct wl_touch *wl_touch;
+ struct {
+ enum touch_state state;
+
+ uint32_t serial;
+ uint32_t time;
+ struct wl_surface *surface;
+ int32_t id;
+ } touch;
+
struct {
int x;
int y;