input: add basic support for touchscreen input

Closes #517
This commit is contained in:
CismonX 2023-07-05 00:19:21 +08:00 committed by Daniel Eklöf
parent 4a73828911
commit d2fcb5343f
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
12 changed files with 364 additions and 22 deletions

View file

@ -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

View file

@ -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
<kbd>tap</kbd>
: Emulates mouse left button click.
<kbd>drag</kbd>
: 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

View file

@ -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 &&

View file

@ -347,6 +347,10 @@ struct config {
bool sixel;
} tweak;
struct {
uint32_t long_press_delay;
} touch;
user_notifications_t notifications;
};

View file

@ -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

View file

@ -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

View file

@ -70,6 +70,9 @@
# hide-when-typing=no
# alternate-scroll-mode=yes
[touch]
# long-press-delay=400
[colors]
# alpha=1.0
# background=002b36

266
input.c
View file

@ -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,
};

View file

@ -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);

View file

@ -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();

View file

@ -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

View file

@ -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;