From d2fcb5343f57c132abec9a21ad89d1505a32c47c Mon Sep 17 00:00:00 2001 From: CismonX Date: Wed, 5 Jul 2023 00:19:21 +0800 Subject: [PATCH] input: add basic support for touchscreen input Closes #517 --- CHANGELOG.md | 2 + README.md | 12 ++ config.c | 20 ++++ config.h | 4 + doc/foot.1.scd | 12 ++ doc/foot.ini.5.scd | 8 ++ foot.ini | 3 + input.c | 266 ++++++++++++++++++++++++++++++++++++++++---- input.h | 1 + tests/test-config.c | 16 +++ wayland.c | 23 +++- wayland.h | 19 ++++ 12 files changed, 364 insertions(+), 22 deletions(-) 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;