diff --git a/CMakeLists.txt b/CMakeLists.txt index 7c201efc..6690c6f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -110,6 +110,7 @@ set(FLUTTER_PI_SRC src/collection.c src/cursor.c src/keyboard.c + src/user_input.c src/plugins/services.c ) @@ -163,7 +164,7 @@ target_compile_options(flutter-pi PRIVATE ${LIBINPUT_CFLAGS} ${LIBUDEV_CFLAGS} ${LIBXKBCOMMON_CFLAGS} - $<$<CONFIG:DEBUG>:-O0 -ggdb> + $<$<CONFIG:DEBUG>:-O0 -ggdb -DDEBUG> ) if (BUILD_TEXT_INPUT_PLUGIN) diff --git a/include/collection.h b/include/collection.h index 2242f157..8044c42d 100644 --- a/include/collection.h +++ b/include/collection.h @@ -5,6 +5,8 @@ #include <stdlib.h> #include <errno.h> #include <stdbool.h> +#include <stdint.h> +#include <assert.h> #include <pthread.h> @@ -404,4 +406,25 @@ static inline void *memdup(const void *restrict src, const size_t n) { #define BMAP_CLEAR(p_bmap, i_bit) ((p_bmap)[(i_bit) / sizeof(*(p_bmap))] &= ~(1 << ((i_bit) & (sizeof(*(p_bmap)) - 1)))) #define BMAP_ZERO(p_bmap, n_bits) (memset((p_bmap), 0, (((n_bits) - 1) / 8) + 1)) +#define min(a, b) ((a) < (b) ? (a) : (b)) +#define max(a, b) ((a) > (b) ? (a) : (b)) + +/** + * @brief Get the current time of the system monotonic clock. + * @returns time in nanoseconds. + */ +static inline uint64_t get_monotonic_time(void) { + struct timespec time; + clock_gettime(CLOCK_MONOTONIC, &time); + return time.tv_nsec + time.tv_sec*1000000000ull; +} + +#ifdef DEBUG +#define DEBUG_ASSERT(__cond) assert(__cond) +#define DEBUG_ASSERT_MSG(__cond, __msg) assert((__msg, (__cond)) +#else +#define DEBUG_ASSERT(__cond) do {} while (false) +#define DEBUG_ASSERT_MSG(__cond, __msg) do {} while (false) +#endif + #endif \ No newline at end of file diff --git a/include/flutter-pi.h b/include/flutter-pi.h index 7820f23c..a44b8476 100644 --- a/include/flutter-pi.h +++ b/include/flutter-pi.h @@ -1,6 +1,8 @@ #ifndef _FLUTTERPI_H #define _FLUTTERPI_H +#define LOG_FLUTTERPI_ERROR(...) fprintf(stderr, "[flutter-pi] " __VA_ARGS__) + #include <limits.h> #include <linux/input.h> #include <stdbool.h> @@ -211,11 +213,17 @@ struct libudev { .skewY = 0, .scaleY = 1, .transY = 0, \ .pers0 = -sin(((double) (deg))/180.0*M_PI), .pers1 = 0, .pers2 = cos(((double) (deg))/180.0*M_PI)}) +/** + * A flutter transformation that rotates any coords around the z-axis, counter-clockwise. + */ #define FLUTTER_ROTZ_TRANSFORMATION(deg) ((FlutterTransformation) \ {.scaleX = cos(((double) (deg))/180.0*M_PI), .skewX = -sin(((double) (deg))/180.0*M_PI), .transX = 0, \ .skewY = sin(((double) (deg))/180.0*M_PI), .scaleY = cos(((double) (deg))/180.0*M_PI), .transY = 0, \ .pers0 = 0, .pers1 = 0, .pers2 = 1}) +/** + * A transformation that is the result of multiplying a with b. + */ #define FLUTTER_MULTIPLIED_TRANSFORMATIONS(a, b) ((FlutterTransformation) \ {.scaleX = a.scaleX * b.scaleX + a.skewX * b.skewY + a.transX * b.pers0, \ .skewX = a.scaleX * b.skewX + a.skewX * b.scaleY + a.transX * b.pers1, \ @@ -227,12 +235,24 @@ struct libudev { .pers1 = a.pers0 * b.skewX + a.pers1 * b.scaleY + a.pers2 * b.pers1, \ .pers2 = a.pers0 * b.transX + a.pers1 * b.transY + a.pers2 * b.pers2}) +/** + * A transformation that is the result of adding a with b. + */ #define FLUTTER_ADDED_TRANSFORMATIONS(a, b) ((FlutterTransformation) \ {.scaleX = a.scaleX + b.scaleX, .skewX = a.skewX + b.skewX, .transX = a.transX + b.transX, \ .skewY = a.skewY + b.skewY, .scaleY = a.scaleY + b.scaleY, .transY = a.transY + b.transY, \ .pers0 = a.pers0 + b.pers0, .pers1 = a.pers1 + b.pers1, .pers2 = a.pers2 + b.pers2 \ }) +/** + * A transformation that is the result of transponating a. + */ +#define FLUTTER_TRANSPONATED_TRANSFORMATION(a) ((FlutterTransformation) \ + {.scaleX = a.scaleX, .skewX = a.skewY, .transX = a.pers0, \ + .skewY = a.skewX, .scaleY = a.scaleY, .transY = a.pers1, \ + .pers0 = a.transX, .pers1 = a.transY, .pers2 = a.pers2, \ + }) + static inline void apply_flutter_transformation( const FlutterTransformation t, double *px, @@ -392,21 +412,8 @@ struct flutterpi { struct compositor *compositor; /// IO - struct { - bool use_paths; - bool disable_text_input; - - glob_t input_devices_glob; -# ifndef BUILD_WITHOUT_UDEV_SUPPORT - struct libudev libudev; -# endif - struct libinput *libinput; - sd_event_source *libinput_event_source; - struct keyboard_config *keyboard_config; - - int64_t next_unused_flutter_device_id; - double cursor_x, cursor_y; - } input; + sd_event_source *user_input_event_source; + struct user_input *user_input; /// flutter stuff struct { @@ -453,14 +460,6 @@ struct platform_message { extern struct flutterpi flutterpi; -struct input_device_data { - int64_t flutter_device_id_offset; - struct keyboard_state *keyboard_state; - double x, y; - int64_t buttons; - uint64_t timestamp; -}; - int flutterpi_fill_view_properties( bool has_orientation, enum device_orientation orientation, diff --git a/include/keyboard.h b/include/keyboard.h index 16eb3a8a..c0761475 100644 --- a/include/keyboard.h +++ b/include/keyboard.h @@ -3,6 +3,8 @@ #include <xkbcommon/xkbcommon.h> +#define LOG_KEYBOARD_ERROR(...) fprintf(stderr, "[keyboard] " __VA_ARGS__) + struct keyboard_config { struct xkb_context *context; struct xkb_keymap *default_keymap; diff --git a/include/user_input.h b/include/user_input.h new file mode 100644 index 00000000..2290fe46 --- /dev/null +++ b/include/user_input.h @@ -0,0 +1,141 @@ +#ifndef USER_INPUT_H_ +#define USER_INPUT_H_ + +#include <xkbcommon/xkbcommon.h> +#include <flutter_embedder.h> + +#define LOG_USER_INPUT_ERROR(...) fprintf(stderr, "[user input] " __VA_ARGS__) +#define MAX_COLLECTED_FLUTTER_POINTER_EVENTS 64 + +#define FLUTTER_POINTER_EVENT(_phase, _timestamp, _x, _y, _device, _signal_kind, _scroll_delta_x, _scroll_delta_y, _device_kind, _buttons) \ + (FlutterPointerEvent) { \ + .struct_size = sizeof(FlutterPointerEvent), \ + .phase = (_phase), \ + .timestamp = (_timestamp), \ + .x = (_x), .y = (_y), \ + .device = (_device), \ + .signal_kind = (_signal_kind), \ + .scroll_delta_x = (_scroll_delta_x), \ + .scroll_delta_y = (_scroll_delta_y), \ + .device_kind = (_device_kind), \ + .buttons = (_buttons) \ + } + +#define FLUTTER_POINTER_TOUCH_ADD_EVENT(_timestamp, _x, _y, _device_id) \ + FLUTTER_POINTER_EVENT(kAdd, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0) + +#define FLUTTER_POINTER_TOUCH_REMOVE_EVENT(_timestamp, _x, _y, _device_id) \ + FLUTTER_POINTER_EVENT(kRemove, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0) + +#define FLUTTER_POINTER_TOUCH_MOVE_EVENT(_timestamp, _x, _y, _device_id) \ + FLUTTER_POINTER_EVENT(kMove, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0) + +#define FLUTTER_POINTER_TOUCH_DOWN_EVENT(_timestamp, _x, _y, _device_id) \ + FLUTTER_POINTER_EVENT(kDown, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0) + +#define FLUTTER_POINTER_TOUCH_UP_EVENT(_timestamp, _x, _y, _device_id) \ + FLUTTER_POINTER_EVENT(kUp, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindTouch, 0) + +#define FLUTTER_POINTER_MOUSE_BUTTON_EVENT(_phase, _timestamp, _x, _y, _device_id, _buttons) \ + FLUTTER_POINTER_EVENT(_phase, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindMouse, _buttons) + +#define FLUTTER_POINTER_MOUSE_ADD_EVENT(_timestamp, _x, _y, _device_id, _buttons) \ + FLUTTER_POINTER_EVENT(kAdd, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindMouse, _buttons) + +#define FLUTTER_POINTER_MOUSE_REMOVE_EVENT(_timestamp, _x, _y, _device_id, _buttons) \ + FLUTTER_POINTER_EVENT(kRemove, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindMouse, _buttons) + +#define FLUTTER_POINTER_REMOVE_EVENT(_timestamp, _x, _y, _device, _buttons) \ + FLUTTER_POINTER_EVENT(kRemove, _timestamp, _x, _y, _device_id, kFlutterPointerSignalKindNone, 0.0, 0.0, kFlutterPointerDeviceKindMouse, _buttons) + +#define FLUTTER_POINTER_MOUSE_MOVE_EVENT(_timestamp, _x, _y, _device_id, _buttons) \ + FLUTTER_POINTER_EVENT( \ + (_buttons) & kFlutterPointerButtonMousePrimary ? kMove : kHover, \ + _timestamp, \ + _x, _y, \ + _device_id, \ + kFlutterPointerSignalKindNone, \ + 0.0, 0.0, \ + kFlutterPointerDeviceKindMouse, \ + _buttons\ + ) + +typedef void (*flutter_pointer_event_callback_t)(void *userdata, const FlutterPointerEvent *events, size_t n_events); + +typedef void (*utf8_character_callback_t)(void *userdata, uint8_t *character); + +typedef void (*xkb_keysym_callback_t)(void *userdata, xkb_keysym_t keysym); + +typedef void (*gtk_keyevent_callback_t)( + void *userdata, + uint32_t unicode_scalar_values, + uint32_t key_code, + uint32_t scan_code, + uint32_t modifiers, + bool is_down +); + +typedef void (*set_cursor_enabled_callback_t)(void *userdata, bool enabled); + +typedef void (*move_cursor_callback_t)(void *userdata, unsigned int x, unsigned int y); + +struct user_input_interface { + flutter_pointer_event_callback_t on_flutter_pointer_event; + utf8_character_callback_t on_utf8_character; + xkb_keysym_callback_t on_xkb_keysym; + gtk_keyevent_callback_t on_gtk_keyevent; + set_cursor_enabled_callback_t on_set_cursor_enabled; + move_cursor_callback_t on_move_cursor; +}; + +struct user_input; + +/** + * @brief Create a new user input instance. Will try to load the default keyboard config from /etc/default/keyboard + * and create a udev-backed libinput instance. + */ +struct user_input *user_input_new( + const struct user_input_interface *interface, + void *userdata, + const FlutterTransformation *display_to_view_transform, + const FlutterTransformation *view_to_display_transform, + unsigned int display_width, + unsigned int display_height +); + +/** + * @brief Destroy this user input instance and free all allocated memory. This will not remove any input devices + * added to flutter and won't invoke any callbacks in the user input interface at all. + */ +void user_input_destroy(struct user_input *input); + +/** + * @brief Set a 3x3 matrix and display width / height so user_input can transform any device coordinates into + * proper flutter view coordinates. (For example to account for a rotated display) + * Will also transform absolute & relative mouse movements. + * + * @param display_to_view_transform will be copied internally. + */ +void user_input_set_transform( + struct user_input *input, + const FlutterTransformation *display_to_view_transform, + const FlutterTransformation *view_to_display_transform, + unsigned int display_width, + unsigned int display_height +); + +/** + * @brief Returns a filedescriptor used for input event notification. The returned + * filedescriptor should be listened to with EPOLLIN | EPOLLRDHUP | EPOLLPRI or equivalent. + * When the fd becomes ready, @ref user_input_on_fd_ready should be called not long after it + * became ready. (libinput somehow relies on that) + */ +int user_input_get_fd(struct user_input *input); + +/** + * @brief Should be called when the fd returned by @ref user_input_get_fd becomes ready. + * The user_input_interface callbacks will be called inside this function. + */ +int user_input_on_fd_ready(struct user_input *input); + +#endif \ No newline at end of file diff --git a/src/flutter-pi.c b/src/flutter-pi.c index eb5f7840..670c7cdf 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -45,6 +45,7 @@ #include <flutter-pi.h> #include <compositor.h> #include <keyboard.h> +#include <user_input.h> #include <platformchannel.h> #include <pluginregistry.h> #include <texture_registry.h> @@ -1178,6 +1179,17 @@ int flutterpi_fill_view_properties( flutterpi.view.display_to_view_transform.transX = flutterpi.display.height; } + if (flutterpi.user_input != NULL) { + // update the user input with the new transforms + user_input_set_transform( + flutterpi.user_input, + &flutterpi.view.display_to_view_transform, + &flutterpi.view.view_to_display_transform, + flutterpi.display.width, + flutterpi.display.height + ); + } + return 0; } @@ -1879,6 +1891,7 @@ int flutterpi_schedule_exit(void) { /************** * USER INPUT * **************/ +/* static int libinput_interface_on_open(const char *path, int flags, void *userdata) { return open(path, flags | O_CLOEXEC); } @@ -2436,65 +2449,167 @@ static struct libinput *try_create_path_backed_libinput(void) { return libinput; } +*/ -static int init_user_input(void) { - sd_event_source *libinput_event_source; - struct keyboard_config *kbdcfg; - struct libinput *libinput; +/************** + * USER INPUT * + **************/ +static void on_flutter_pointer_event(void *userdata, const FlutterPointerEvent *events, size_t n_events) { + FlutterEngineResult engine_result; + struct flutterpi *flutterpi; + + flutterpi = userdata; + + engine_result = flutterpi->flutter.libflutter_engine.FlutterEngineSendPointerEvent( + flutterpi->flutter.engine, + events, + n_events + ); + + if (engine_result != kSuccess) { + LOG_FLUTTERPI_ERROR("Error sending touchscreen / mouse events to flutter. FlutterEngineSendPointerEvent: %s\n", FLUTTER_RESULT_TO_STRING(engine_result)); + //flutterpi_schedule_exit(flutterpi); + } +} + +static void on_utf8_character(void *userdata, uint8_t *character) { + FlutterEngineResult engine_result; + struct flutterpi *flutterpi; int ok; - libinput_event_source = NULL; - kbdcfg = NULL; - libinput = NULL; + flutterpi = userdata; - if (flutterpi.input.use_paths == false) { - libinput = try_create_udev_backed_libinput(); +#ifdef BUILD_TEXT_INPUT_PLUGIN + ok = textin_on_utf8_char(character); + if (ok != 0) { + LOG_FLUTTERPI_ERROR("Error handling keyboard event. textin_on_utf8_char: %s\n", strerror(ok)); + //flutterpi_schedule_exit(flutterpi); } +#endif +} - if (libinput == NULL) { - libinput = try_create_path_backed_libinput(); +static void on_xkb_keysym(void *userdata, xkb_keysym_t keysym) { + FlutterEngineResult engine_result; + struct flutterpi *flutterpi; + int ok; + + flutterpi = userdata; + +#ifdef BUILD_TEXT_INPUT_PLUGIN + ok = textin_on_xkb_keysym(keysym); + if (ok != 0) { + LOG_FLUTTERPI_ERROR("Error handling keyboard event. textin_on_xkb_keysym: %s\n", strerror(ok)); + //flutterpi_schedule_exit(flutterpi); } +#endif +} + +static void on_gtk_keyevent( + void *userdata, + uint32_t unicode_scalar_values, + uint32_t key_code, + uint32_t scan_code, + uint32_t modifiers, + bool is_down +) { + FlutterEngineResult engine_result; + struct flutterpi *flutterpi; + int ok; + + flutterpi = userdata; + +#ifdef BUILD_RAW_KEYBOARD_PLUGIN + ok = rawkb_send_gtk_keyevent( + unicode_scalar_values, + key_code, + scan_code, + modifiers, + is_down + ); + if (ok != 0) { + LOG_FLUTTERPI_ERROR("Error handling keyboard event. rawkb_send_gtk_keyevent: %s\n", strerror(ok)); + //flutterpi_schedule_exit(flutterpi); + } +#endif +} + +static void on_set_cursor_enabled(void *userdata, bool enabled) { + struct flutterpi *flutterpi; + int ok; + + flutterpi = userdata; + + ok = compositor_apply_cursor_state( + enabled, + flutterpi->view.rotation, + flutterpi->display.pixel_ratio + ); + if (ok != 0) { + LOG_FLUTTERPI_ERROR("Error enabling / disabling mouse cursor. compositor_apply_cursor_state: %s\n", strerror(ok)); + } +} + +static void on_move_cursor(void *userdata, unsigned int x, unsigned int y) { + struct flutterpi *flutterpi; + int ok; + + flutterpi = userdata; + + ok = compositor_set_cursor_pos(x, y); + if (ok != 0) { + LOG_FLUTTERPI_ERROR("Error moving mouse cursor. compositor_set_cursor_pos: %s\n", strerror(ok)); + } +} + +const static struct user_input_interface user_input_interface = { + .on_flutter_pointer_event = on_flutter_pointer_event, + .on_utf8_character = on_utf8_character, + .on_xkb_keysym = on_xkb_keysym, + .on_gtk_keyevent = on_gtk_keyevent, + .on_set_cursor_enabled = on_set_cursor_enabled, + .on_move_cursor = on_move_cursor +}; + +static int on_user_input_fd_ready(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + struct user_input *input = userdata; + return user_input_on_fd_ready(input); +} + +static int init_user_input(void) { + struct user_input *input; + sd_event_source *event_source; + int ok; - if (libinput != NULL) { + event_source = NULL; + + input = user_input_new( + &user_input_interface, + &flutterpi, + &flutterpi.view.display_to_view_transform, + &flutterpi.view.view_to_display_transform, + flutterpi.display.width, + flutterpi.display.height + ); + if (input == NULL) { + LOG_FLUTTERPI_ERROR("Couldn't initialize user input. flutter-pi will run without user input.\n"); + } else { ok = sd_event_add_io( flutterpi.event_loop, - &libinput_event_source, - libinput_get_fd(libinput), + &event_source, + user_input_get_fd(input), EPOLLIN | EPOLLRDHUP | EPOLLPRI, - on_libinput_ready, - NULL + on_user_input_fd_ready, + input ); if (ok < 0) { - fprintf(stderr, "[flutter-pi] Could not add libinput callback to main loop. sd_event_add_io: %s\n", strerror(-ok)); -# ifndef BUILD_WITHOUT_UDEV_SUPPORT - if (libinput_get_user_data(libinput) != NULL) { - struct udev *udev = libinput_get_user_data(libinput); - libinput_unref(libinput); - flutterpi.input.libudev.udev_unref(udev); - } else { - libinput_unref(libinput); - } -# else - libinput_unref(libinput); -# endif - return -ok; + LOG_FLUTTERPI_ERROR("Couldn't listen for user input. flutter-pi will run without user input. sd_event_add_io: %s\n", strerror(-ok)); + user_input_destroy(input); + input = NULL; } - -#ifdef BUILD_TEXT_INPUT_PLUGIN - if (flutterpi.input.disable_text_input == false) { - kbdcfg = keyboard_config_new(); - if (kbdcfg == NULL) { - fprintf(stderr, "[flutter-pi] Could not initialize keyboard configuration. Flutter-pi will run without text/raw keyboard input.\n"); - } - } -#endif - } else { - fprintf(stderr, "[flutter-pi] Could not initialize input. Flutter-pi will run without user input.\n"); } - - flutterpi.input.libinput = libinput; - flutterpi.input.libinput_event_source = libinput_event_source; - flutterpi.input.keyboard_config = kbdcfg; + + flutterpi.user_input = input; + flutterpi.user_input_event_source = event_source; return 0; } @@ -2540,12 +2655,10 @@ static bool setup_paths(void) { } static bool parse_cmd_args(int argc, char **argv) { - glob_t input_devices_glob = {0}; bool input_specified = false; int opt; int longopt_index = 0; int runtime_mode_int = kDebug; - int disable_text_input_int = false; int ok; struct option long_options[] = { @@ -2553,7 +2666,6 @@ static bool parse_cmd_args(int argc, char **argv) { {"input", required_argument, NULL, 'i'}, {"orientation", required_argument, NULL, 'o'}, {"rotation", required_argument, NULL, 'r'}, - {"no-text-input", no_argument, &disable_text_input_int, true}, {"dimensions", required_argument, NULL, 'd'}, {"help", no_argument, 0, 'h'}, {0, 0, 0, 0} @@ -2568,10 +2680,6 @@ static bool parse_cmd_args(int argc, char **argv) { case 0: // flag was encountered. just continue break; - case 'i': - glob(optarg, GLOB_BRACE | GLOB_TILDE | (input_specified ? GLOB_APPEND : 0), NULL, &input_devices_glob); - input_specified = true; - break; case 'o': if (STREQ(optarg, "portrait_up")) { @@ -2648,10 +2756,6 @@ static bool parse_cmd_args(int argc, char **argv) { } } - if (!input_specified) { - // user specified no input devices. use "/dev/input/event*"". - glob("/dev/input/event*", GLOB_BRACE | GLOB_TILDE, NULL, &input_devices_glob); - } if (optind >= argc) { fprintf(stderr, "error: expected asset bundle path after options.\n"); @@ -2659,11 +2763,8 @@ static bool parse_cmd_args(int argc, char **argv) { return false; } - flutterpi.input.use_paths = input_specified; flutterpi.flutter.asset_bundle_path = strdup(argv[optind]); flutterpi.flutter.runtime_mode = runtime_mode_int; - flutterpi.input.disable_text_input = disable_text_input_int; - flutterpi.input.input_devices_glob = input_devices_glob; argv[optind] = argv[0]; flutterpi.flutter.engine_argc = argc - optind; diff --git a/src/keyboard.c b/src/keyboard.c index a7b86234..c405bbfb 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -73,11 +73,9 @@ static char *get_value_allocated(const char *varname, const char *buffer) { ok = find_var_offset_in_string(varname, buffer, &match); if (ok != 0) { - fprintf(stderr, "Error finding variable in buffer: find_var_offset_in_string: %s\n", strerror(ok)); errno = ok; return NULL; } else if ((match.rm_so == -1) || (match.rm_eo == -1)) { - fprintf(stderr, "Could not find variable in buffer.\n"); errno = EINVAL; return NULL; } @@ -145,18 +143,40 @@ static char *load_file(const char *path) { } static struct xkb_keymap *load_default_keymap(struct xkb_context *context) { - char *file = load_file("/etc/default/keyboard"); + struct xkb_keymap *keymap; + char *file, *xkbmodel, *xkblayout, *xkbvariant, *xkboptions; + + file = load_file("/etc/default/keyboard"); if (file == NULL) { - perror("[keyboard] Could not load default keyboard configuration from \"/etc/default/keyboard\". "); - return NULL; - } + LOG_KEYBOARD_ERROR("Could not load keyboard configuration from \"/etc/default/keyboard\". Default keyboard config will be used. load_file: %s\n", strerror(errno)); + xkbmodel = NULL; + xkblayout = NULL; + xkbvariant = NULL; + xkboptions = NULL; + } else { + // we have a config file, load its properties + xkbmodel = get_value_allocated("XKBMODEL", file); + if (xkbmodel == NULL) { + LOG_KEYBOARD_ERROR("Could not find \"XKBMODEL\" property inside \"/etc/default/keyboard\". Default value will be used."); + } + + xkblayout = get_value_allocated("XKBLAYOUT", file); + if (xkblayout == NULL) { + LOG_KEYBOARD_ERROR("Could not find \"XKBLAYOUT\" property inside \"/etc/default/keyboard\". Default value will be used."); + } - char *xkbmodel = get_value_allocated("XKBMODEL", file); - char *xkblayout = get_value_allocated("XKBLAYOUT", file); - char *xkbvariant = get_value_allocated("XKBVARIANT", file); - char *xkboptions = get_value_allocated("XKBOPTIONS", file); + xkbvariant = get_value_allocated("XKBVARIANT", file); + if (xkbvariant == NULL) { + LOG_KEYBOARD_ERROR("Could not find \"XKBVARIANT\" property inside \"/etc/default/keyboard\". Default value will be used."); + } + + xkboptions = get_value_allocated("XKBOPTIONS", file); + if (xkboptions == NULL) { + LOG_KEYBOARD_ERROR("Could not find \"XKBOPTIONS\" property inside \"/etc/default/keyboard\". Default value will be used."); + } - free(file); + free(file); + } struct xkb_rule_names names = { .rules = NULL, @@ -166,7 +186,7 @@ static struct xkb_keymap *load_default_keymap(struct xkb_context *context) { .options = xkboptions }; - struct xkb_keymap *keymap = xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS); + keymap = xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS); if (xkbmodel != NULL) free(xkbmodel); if (xkblayout != NULL) free(xkblayout); @@ -174,15 +194,23 @@ static struct xkb_keymap *load_default_keymap(struct xkb_context *context) { if (xkboptions != NULL) free(xkboptions); if (keymap == NULL) { - fprintf(stderr, "[keyboard] Could not load default keymap.\n"); + LOG_KEYBOARD_ERROR("Could not create xkb keymap."); } return keymap; } static struct xkb_compose_table *load_default_compose_table(struct xkb_context *context) { + struct xkb_compose_table *tbl; + setlocale(LC_ALL, ""); - return xkb_compose_table_new_from_locale(context, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + + tbl = xkb_compose_table_new_from_locale(context, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); + if (tbl == NULL) { + LOG_KEYBOARD_ERROR("Could not create compose table from locale.\n"); + } + + return tbl; } @@ -202,6 +230,7 @@ struct keyboard_config *keyboard_config_new(void) { ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); if (ctx == NULL) { + LOG_KEYBOARD_ERROR("Could not create XKB context.\n"); goto fail_free_cfg; } @@ -262,16 +291,19 @@ struct keyboard_state *keyboard_state_new( xkb_state = xkb_state_new(keymap_override != NULL ? keymap_override : config->default_keymap); if (xkb_state == NULL) { + LOG_KEYBOARD_ERROR("Could not create new XKB state.\n"); goto fail_free_state; } plain_xkb_state = xkb_state_new(keymap_override != NULL ? keymap_override : config->default_keymap); if (plain_xkb_state == NULL) { + LOG_KEYBOARD_ERROR("Could not create new XKB state.\n"); goto fail_free_xkb_state; } compose_state = xkb_compose_state_new(compose_table_override != NULL ? compose_table_override : config->default_compose_table, XKB_COMPOSE_STATE_NO_FLAGS); if (compose_state == NULL) { + LOG_KEYBOARD_ERROR("Could not create new XKB compose state.\n"); goto fail_free_xkb_state; } diff --git a/src/plugins/text_input.c b/src/plugins/text_input.c index 0a855f34..9773bbc0 100644 --- a/src/plugins/text_input.c +++ b/src/plugins/text_input.c @@ -747,15 +747,6 @@ int client_show_autocorrection_prompt_rect( /** * Text Input Model functions. */ - -static inline int min(int a, int b) { - return a < b? a : b; -} - -static inline int max(int a, int b) { - return a > b? a : b; -} - static inline int selection_start(void) { return min(text_input.selection_base, text_input.selection_extent); } diff --git a/src/user_input.c b/src/user_input.c new file mode 100644 index 00000000..761a73ee --- /dev/null +++ b/src/user_input.c @@ -0,0 +1,1048 @@ +#include <stdlib.h> +#include <stdio.h> +#include <memory.h> +#include <fcntl.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <math.h> + +#include <linux/input-event-codes.h> +#include <systemd/sd-event.h> +#include <libinput.h> + +#include <flutter-pi.h> +#include <collection.h> +#include <keyboard.h> +#include <user_input.h> + +struct input_device_data { + int64_t flutter_device_id_offset; + struct keyboard_state *keyboard_state; + double x, y; + int64_t buttons; + uint64_t timestamp; + + /** + * @brief Whether libinput has ever emitted any pointer / mouse events + * for this device. + * + * Only applies to devices which have LIBINPUT_DEVICE_CAP_POINTER. + */ + bool has_emitted_pointer_events; +}; + +struct user_input { + struct user_input_interface interface; + void *userdata; + + struct libinput *libinput; + struct keyboard_config *kbdcfg; + int64_t next_unused_flutter_device_id; + + /// TODO: Maybe fetch the transform, display dimensions, cursor pos dynamically using a callback instead? + + /** + * @brief transforms normalized display coordinates (0 .. display_width-1, 0 .. display_height-1) to the coordinates + * used in the flutter pointer events + */ + FlutterTransformation display_to_view_transform; + FlutterTransformation view_to_display_transform_nontranslating; + unsigned int display_width; + unsigned int display_height; + + + /** + * @brief The number of devices connected that want a mouse cursor. + * libinput calls them pointer devices, flutter calls them mice. + */ + unsigned int n_cursor_devices; + /** + * @brief The flutter device id of the mouse cursor, if @ref n_cursor_devices > 0. + */ + int64_t cursor_flutter_device_id; + /** + * @brief The current mouse cursor position in floating point display coordinates (0 .. display_width-1, 0 .. display_height-1) + */ + double cursor_x, cursor_y; + + /** + * @brief Buffer of collected flutter pointer events, since we can send multiple events at once to flutter. + */ + FlutterPointerEvent collected_flutter_pointer_events[MAX_COLLECTED_FLUTTER_POINTER_EVENTS]; + /** + * @brief Number of pointer events currently contained in @ref collected_flutter_pointer_events. + */ + size_t n_collected_flutter_pointer_events; +}; + +// libinput interface +static int open_restricted(const char *path, int flags, void *userdata) { + (void) userdata; + return open(path, flags | O_CLOEXEC); +} + +static void close_restricted(int fd, void *userdata) { + (void) userdata; + close(fd); +} + +static const struct libinput_interface libinput_interface = { + .open_restricted = open_restricted, + .close_restricted = close_restricted +}; + +struct user_input *user_input_new( + const struct user_input_interface *interface, + void *userdata, + const FlutterTransformation *display_to_view_transform, + const FlutterTransformation *view_to_display_transform, + unsigned int display_width, + unsigned int display_height +) { + struct keyboard_config *kbdcfg; + struct user_input *input; + sd_event_source *libinput_event_source; + struct libinput *libinput; + struct udev *udev; + int ok; + + input = malloc(sizeof *input); + if (input == NULL) { + goto fail_return_null; + } + + udev = udev_new(); + if (udev == NULL) { + perror("[flutter-pi] Could not create udev instance. udev_new"); + goto fail_free_input; + } + + libinput = libinput_udev_create_context( + &libinput_interface, + input, + udev + ); + if (libinput == NULL) { + perror("[flutter-pi] Could not create libinput instance. libinput_udev_create_context"); + goto fail_unref_udev; + } + + udev_unref(udev); + + ok = libinput_udev_assign_seat(libinput, "seat0"); + if (ok < 0) { + fprintf(stderr, "[flutter-pi] Could not assign udev seat to libinput instance. libinput_udev_assign_seat: %s\n", strerror(-ok)); + goto fail_unref_libinput; + } + +#ifdef BUILD_TEXT_INPUT_PLUGIN + kbdcfg = keyboard_config_new(); + if (kbdcfg == NULL) { + fprintf(stderr, "[flutter-pi] Could not initialize keyboard configuration. Flutter-pi will run without text/raw keyboard input.\n"); + } +#else + kbdcfg = NULL; +#endif + + input->interface = *interface; + input->userdata = userdata; + + input->libinput = libinput; + input->kbdcfg = kbdcfg; + input->next_unused_flutter_device_id = 0; + + user_input_set_transform( + input, + display_to_view_transform, + view_to_display_transform, + display_width, + display_height + ); + + input->n_cursor_devices = 0; + input->cursor_flutter_device_id = -1; + input->cursor_x = 0.0; + input->cursor_y = 0.0; + + input->n_collected_flutter_pointer_events = 0; + + return input; + + + fail_unref_libinput: + libinput_unref(libinput); + goto fail_free_input; + + fail_unref_udev: + udev_unref(udev); + + fail_free_input: + free(input); + + fail_return_null: + return NULL; +} + +void user_input_destroy(struct user_input *input) { + DEBUG_ASSERT(input != NULL); + + /// TODO: Destroy all the input device data, maybe add an additional + /// parameter to indicate whether any flutter device removal events should be + /// emitted. + + if (input->kbdcfg != NULL) { + keyboard_config_destroy(input->kbdcfg); + } + libinput_unref(input->libinput); + free(input); +} + +void user_input_set_transform( + struct user_input *input, + const FlutterTransformation *display_to_view_transform, + const FlutterTransformation *view_to_display_transform, + unsigned int display_width, + unsigned int display_height +) { + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(display_to_view_transform != NULL); + DEBUG_ASSERT(view_to_display_transform != NULL); + + input->display_to_view_transform = *display_to_view_transform; + input->view_to_display_transform_nontranslating = *view_to_display_transform; + input->view_to_display_transform_nontranslating.transX = 0.0; + input->view_to_display_transform_nontranslating.transY = 0.0; + input->display_width = display_width; + input->display_height = display_height; +} + +int user_input_get_fd(struct user_input *input) { + DEBUG_ASSERT(input != NULL); + return libinput_get_fd(input->libinput); +} + + +static void flush_pointer_events(struct user_input *input) { + DEBUG_ASSERT(input != NULL); + + if (input->n_collected_flutter_pointer_events > 0) { + input->interface.on_flutter_pointer_event( + input->userdata, + input->collected_flutter_pointer_events, + input->n_collected_flutter_pointer_events + ); + + input->n_collected_flutter_pointer_events = 0; + } +} + +static void emit_pointer_events(struct user_input *input, const FlutterPointerEvent *events, size_t n_events) { + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(events != NULL); + + size_t to_copy; + + while (n_events > 0) { + // if the internal buffer is full, flush it + if (input->n_collected_flutter_pointer_events == MAX_COLLECTED_FLUTTER_POINTER_EVENTS) { + flush_pointer_events(input); + } + + // how many pointer events we can copy into the internal pointer event buffer? + to_copy = min(n_events, MAX_COLLECTED_FLUTTER_POINTER_EVENTS - input->n_collected_flutter_pointer_events); + + // copy into the internal pointer event buffer + memcpy( + input->collected_flutter_pointer_events + input->n_collected_flutter_pointer_events, + events, + to_copy * sizeof(FlutterPointerEvent) + ); + + // advance n_events so it's now the number of remaining unemitted events + n_events -= to_copy; + + // advance events so it points to the first remaining unemitted event + events += to_copy; + + // advance the number of stored pointer events + input->n_collected_flutter_pointer_events += to_copy; + } +} + +/** + * @brief Called when input->n_cursor_devices was increased to maybe enable the mouse cursor + * it it isn't yet enabled. + */ +static void maybe_enable_mouse_cursor(struct user_input *input, uint64_t timestamp) { + DEBUG_ASSERT(input != NULL); + + if (input->n_cursor_devices == 1) { + input->cursor_flutter_device_id = input->next_unused_flutter_device_id++; + + emit_pointer_events( + input, + &FLUTTER_POINTER_MOUSE_ADD_EVENT( + timestamp, + input->cursor_x, input->cursor_y, + input->cursor_flutter_device_id, + 0 + ), + 1 + ); + } +} + +/** + * @brief Called when input->n_cursor_devices was decreased to maybe disable the mouse cursor. + */ +static void maybe_disable_mouse_cursor(struct user_input *input, uint64_t timestamp) { + DEBUG_ASSERT(input != NULL); + + if (input->n_cursor_devices == 0) { + emit_pointer_events( + input, + &FLUTTER_POINTER_MOUSE_REMOVE_EVENT( + timestamp, + input->cursor_x, input->cursor_y, + input->cursor_flutter_device_id, + 0 + ), + 1 + ); + } +} + + +static int on_device_added(struct user_input *input, struct libinput_event *event, uint64_t timestamp) { + struct input_device_data *data; + struct libinput_device *device; + int64_t device_id; + int ok; + + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + + device = libinput_event_get_device(event); + + data = malloc(sizeof *data); + if (data == NULL) { + return ENOMEM; + } + + data->flutter_device_id_offset = input->next_unused_flutter_device_id; + data->keyboard_state = NULL; + data->x = 0.0; + data->y = 0.0; + data->buttons = 0; + data->timestamp = timestamp; + data->has_emitted_pointer_events = false; + + libinput_device_set_user_data(device, data); + + if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER)) { + // no special things to do here + // mouse pointer will be added as soon as the device actually sends a + // mouse event, as some devices will erroneously have a LIBINPUT_DEVICE_CAP_POINTER + // even though they aren't mice. (My keyboard for example is a mouse smh) + } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_TOUCH)) { + // add all touch slots as individual touch devices to flutter + for (int i = 0; i < libinput_device_touch_get_touch_count(device); i++) { + device_id = input->next_unused_flutter_device_id++; + + emit_pointer_events( + input, + &FLUTTER_POINTER_TOUCH_ADD_EVENT( + timestamp, + 0.0, 0.0, + device_id + ), + 1 + ); + } + } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_KEYBOARD)) { + // create a new keyboard state for this keyboard + if (input->kbdcfg) { + data->keyboard_state = keyboard_state_new(input->kbdcfg, NULL, NULL); + } else { + // If we don't have a keyboard config + data->keyboard_state = NULL; + } + } else { + // We don't handle this device, so we don't need the data. + libinput_device_set_user_data(device, NULL); + free(data); + } + + return 0; +} + +static int on_device_removed(struct user_input *input, struct libinput_event *event, uint64_t timestamp) { + struct input_device_data *data; + struct libinput_device *device; + + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + + device = libinput_event_get_device(event); + data = libinput_device_get_user_data(device); + + if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_POINTER)) { + // if we don't have a mouse cursor added to flutter yet, add one + if (data->has_emitted_pointer_events) { + input->n_cursor_devices--; + maybe_disable_mouse_cursor(input, timestamp); + } + } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_TOUCH)) { + // add all touch slots as individual touch devices to flutter + for (int i = 0; i < libinput_device_touch_get_touch_count(device); i++) { + emit_pointer_events( + input, + &FLUTTER_POINTER_TOUCH_REMOVE_EVENT( + timestamp, + 0.0, 0.0, + data->flutter_device_id_offset + i + ), + 1 + ); + } + } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_KEYBOARD)) { + // create a new keyboard state for this keyboard + if (data->keyboard_state != NULL) { + keyboard_state_destroy(data->keyboard_state); + } + } + + if (data != NULL) { + free(data); + } + + libinput_device_set_user_data(device, NULL); + + return 0; +} + +static int on_key_event(struct user_input *input, struct libinput_event *event) { + struct libinput_event_keyboard *key_event; + struct input_device_data *data; + struct libinput_device *device; + struct keyboard_modifier_state mods; + enum libinput_key_state key_state; + xkb_keysym_t keysym; + uint32_t codepoint, plain_codepoint; + uint16_t evdev_keycode; + int ok; + + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + + key_event = libinput_event_get_keyboard_event(event); + device = libinput_event_get_device(event); + data = libinput_device_get_user_data(device); + + evdev_keycode = (uint16_t) libinput_event_keyboard_get_key(key_event); + key_state = libinput_event_keyboard_get_key_state(key_event); + + // If we don't have a keyboard state (for example if we couldn't load /etc/default/keyboard) + // we just return here. + if (data->keyboard_state == NULL) { + return 0; + } + + // Let the keyboard advance its statemachine. + // keysym/codepoint are 0 when none were emitted. + ok = keyboard_state_process_key_event( + data->keyboard_state, + evdev_keycode, + (int32_t) key_state, + &keysym, + &codepoint + ); + if (ok != 0) { + return ok; + } + + // GTK keyevent needs the plain codepoint for some reason. + /// TODO: Maybe remove the evdev_value parameter? + plain_codepoint = keyboard_state_get_plain_codepoint(data->keyboard_state, evdev_keycode, 1); + + // call the GTK keyevent callback. + /// TODO: Simplify the meta state construction. + input->interface.on_gtk_keyevent( + input->userdata, + plain_codepoint, + (uint32_t) keysym, + evdev_keycode + (uint32_t) 8, + keyboard_state_is_shift_active(data->keyboard_state) + | (keyboard_state_is_capslock_active(data->keyboard_state) << 1) + | (keyboard_state_is_ctrl_active(data->keyboard_state) << 2) + | (keyboard_state_is_alt_active(data->keyboard_state) << 3) + | (keyboard_state_is_numlock_active(data->keyboard_state) << 4) + | (keyboard_state_is_meta_active(data->keyboard_state) << 28), + key_state + ); + + // Call the UTF8 character callback if we've got a codepoint. + // Code very similiar to that of the linux kernel in drivers/tty/vt/keyboard.c, to_utf8 + if (codepoint) { + if (codepoint < 0x80) { + // we emit UTF8 unconditionally here, + // maybe we should check if codepoint is a control character? + if (isprint(codepoint)) { + input->interface.on_utf8_character( + input->userdata, + (uint8_t[1]) {codepoint} + ); + } + } else if (codepoint < 0x800) { + input->interface.on_utf8_character( + input->userdata, + (uint8_t[2]) { + 0xc0 | (codepoint >> 6), + 0x80 | (codepoint & 0x3f) + } + ); + } else if (codepoint < 0x10000) { + // the console keyboard driver of the linux kernel checks + // at this point whether `codepoint` is a UTF16 high surrogate (U+D800 to U+DFFF) + // or U+FFFF and returns without emitting UTF8 in that case. + // don't know whether we should do this here too + input->interface.on_utf8_character( + input->userdata, + (uint8_t[3]) { + 0xe0 | (codepoint >> 12), + 0x80 | ((codepoint >> 6) & 0x3f), + 0x80 | (codepoint & 0x3f) + } + ); + } else if (codepoint < 0x110000) { + input->interface.on_utf8_character( + input->userdata, + (uint8_t[4]) { + 0xf0 | (codepoint >> 18), + 0x80 | ((codepoint >> 12) & 0x3f), + 0x80 | ((codepoint >> 6) & 0x3f), + 0x80 | (codepoint & 0x3f) + } + ); + } + } + + // Call the XKB keysym callback if we've got a keysym. + if (keysym) { + input->interface.on_xkb_keysym(input->userdata, keysym); + } + + return 0; +} + +static int on_mouse_motion_event(struct user_input *input, struct libinput_event *event) { + struct libinput_event_pointer *pointer_event; + struct input_device_data *data; + struct libinput_device *device; + uint64_t timestamp; + double new_cursor_x, new_cursor_y; + double dx, dy; + + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + + pointer_event = libinput_event_get_pointer_event(event); + device = libinput_event_get_device(event); + data = libinput_device_get_user_data(device); + timestamp = libinput_event_pointer_get_time_usec(pointer_event); + + data->timestamp = timestamp; + + dx = libinput_event_pointer_get_dx(pointer_event); + dy = libinput_event_pointer_get_dy(pointer_event); + + // transform the deltas. when the display is rotated + // we want the mouse to move in different directions as well. + // the dx and dy is still in display (not view) coordinates though, + // we only changed the movement direction. + apply_flutter_transformation(input->view_to_display_transform_nontranslating, &dx, &dy); + + new_cursor_x = input->cursor_x + dx; + new_cursor_y = input->cursor_y + dy; + + // check if we're ran over the display boundaries. + if (new_cursor_x < 0.0) { + new_cursor_x = 0.0; + } else if (new_cursor_x > input->display_width - 1) { + new_cursor_x = input->display_width - 1; + } + + if (new_cursor_y < 0.0) { + new_cursor_y = 0.0; + } else if (new_cursor_y > input->display_height - 1) { + new_cursor_y = input->display_height - 1; + } + + input->cursor_x = new_cursor_x; + input->cursor_y = new_cursor_y; + + // transform the cursor pos to view (flutter) coordinates. + apply_flutter_transformation(input->display_to_view_transform, &new_cursor_x, &new_cursor_y); + + if (data->has_emitted_pointer_events == false) { + data->has_emitted_pointer_events = true; + input->n_cursor_devices++; + maybe_enable_mouse_cursor(input, timestamp); + } + + // send the pointer event to flutter. + emit_pointer_events( + input, + &FLUTTER_POINTER_MOUSE_MOVE_EVENT( + timestamp, + new_cursor_x, + new_cursor_y, + data->flutter_device_id_offset, + data->buttons + ), + 1 + ); + + // we don't invoke the interfaces' mouse move callback here, since we + // can have multiple mouse motion events per process_libinput_events + // and we don't want to invoke the callback each time. + // instead, we call it in user_input_on_fd_ready if the cursors + // display coordinates changed. + + return 0; +} + +static int on_mouse_motion_absolute_event(struct user_input *input, struct libinput_event *event) { + struct libinput_event_pointer *pointer_event; + struct input_device_data *data; + struct libinput_device *device; + uint64_t timestamp; + double x, y; + + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + + pointer_event = libinput_event_get_pointer_event(event); + device = libinput_event_get_device(event); + data = libinput_device_get_user_data(device); + timestamp = libinput_event_pointer_get_time_usec(pointer_event); + + // get the new mouse position in display coordinates + x = libinput_event_pointer_get_absolute_x_transformed(pointer_event, input->display_width - 1); + y = libinput_event_pointer_get_absolute_y_transformed(pointer_event, input->display_height - 1); + + /// FIXME: Why do we store the coordinates here? + data->x = x; + data->y = y; + data->timestamp = timestamp; + + // update the "global" cursor position + input->cursor_x = x; + input->cursor_y = y; + + // transform x & y to view (flutter) coordinates + apply_flutter_transformation(input->display_to_view_transform, &x, &y); + + if (data->has_emitted_pointer_events == false) { + data->has_emitted_pointer_events = true; + input->n_cursor_devices++; + maybe_enable_mouse_cursor(input, timestamp); + } + + emit_pointer_events( + input, + &FLUTTER_POINTER_MOUSE_MOVE_EVENT( + timestamp, + x, y, + data->flutter_device_id_offset, + data->buttons + ), + 1 + ); + + return 0; +} + +static int on_mouse_button_event(struct user_input *input, struct libinput_event *event) { + struct libinput_event_pointer *pointer_event; + enum libinput_button_state button_state; + struct input_device_data *data; + struct libinput_device *device; + FlutterPointerPhase pointer_phase; + uint64_t timestamp; + uint16_t evdev_code; + int64_t flutter_button; + int64_t new_flutter_button_state; + double x, y; + + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + + pointer_event = libinput_event_get_pointer_event(event); + device = libinput_event_get_device(event); + data = libinput_device_get_user_data(device); + timestamp = libinput_event_pointer_get_time_usec(pointer_event); + evdev_code = (uint16_t) libinput_event_pointer_get_button(pointer_event); + button_state = libinput_event_pointer_get_button_state(pointer_event); + + if (data->has_emitted_pointer_events == false) { + data->has_emitted_pointer_events = true; + input->n_cursor_devices++; + maybe_enable_mouse_cursor(input, timestamp); + } + + // find out the flutter mouse button for this event + if (evdev_code == BTN_LEFT) { + flutter_button = kFlutterPointerButtonMousePrimary; + } else if (evdev_code == BTN_RIGHT) { + flutter_button = kFlutterPointerButtonMouseSecondary; + } else if (evdev_code == BTN_MIDDLE) { + flutter_button = kFlutterPointerButtonMouseMiddle; + } else if (evdev_code == BTN_BACK) { + flutter_button = kFlutterPointerButtonMouseBack; + } else if (evdev_code == BTN_FORWARD) { + flutter_button = kFlutterPointerButtonMouseForward; + } else { + flutter_button = 0; + } + + // advance our button state, which is just a bitmap + new_flutter_button_state = data->buttons; + if (button_state == LIBINPUT_BUTTON_STATE_RELEASED) { + // remove the released button from the button state + new_flutter_button_state &= ~flutter_button; + } else { + // add the pressed button to the button state. + // note that libinput doesn't emit key repeat events. + new_flutter_button_state |= flutter_button; + } + + // if our button state changed, + // emit a pointer event + if (new_flutter_button_state != data->buttons) { + if (new_flutter_button_state == 0) { + // no buttons are pressed anymore. + pointer_phase = kUp; + } else if (data->buttons == 0) { + // previously, there were no buttons pressed. + // now, at least 1 is pressed. + pointer_phase = kDown; + } else { + // some button was pressed or released, + // but it + pointer_phase = kMove; + } + + x = input->cursor_x; + y = input->cursor_y; + + // since the stored coords are in display, not view coordinates, + // we need to transform them again + apply_flutter_transformation(input->display_to_view_transform, &x, &y); + + emit_pointer_events( + input, + &FLUTTER_POINTER_MOUSE_BUTTON_EVENT( + pointer_phase, + timestamp, + x, y, + data->flutter_device_id_offset, + new_flutter_button_state + ), + 1 + ); + + // finally apply the new button state + data->buttons = new_flutter_button_state; + } + + return 0; +} + +static int on_mouse_axis_event(struct user_input *input, struct libinput_event *event) { + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + return 0; +} + +static int on_touch_down(struct user_input *input, struct libinput_event *event) { + struct libinput_event_touch *touch_event; + struct input_device_data *data; + uint64_t timestamp; + int64_t device_id; + double x, y; + int slot; + + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + + data = libinput_device_get_user_data(libinput_event_get_device(event)); + touch_event = libinput_event_get_touch_event(event); + timestamp = libinput_event_touch_get_time_usec(touch_event); + + // get the multitouch slot for this event + // can return -1 when the device is a single touch device + slot = libinput_event_touch_get_slot(touch_event); + if (slot == -1) { + slot = 0; + } + + device_id = data->flutter_device_id_offset + slot; + + x = libinput_event_touch_get_x_transformed(touch_event, input->display_width - 1); + y = libinput_event_touch_get_y_transformed(touch_event, input->display_height - 1); + + // transform the display coordinates to view (flutter) coordinates + apply_flutter_transformation(input->display_to_view_transform, &x, &y); + + // emit the flutter pointer event + emit_pointer_events(input, &FLUTTER_POINTER_TOUCH_DOWN_EVENT(timestamp, x, y, device_id), 1); + + // alter our device state + data->x = x; + data->y = y; + data->timestamp = timestamp; + + return 0; +} + +static int on_touch_up(struct user_input *input, struct libinput_event *event) { + struct libinput_event_touch *touch_event; + struct input_device_data *data; + uint64_t timestamp; + int64_t device_id; + int slot; + + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + + data = libinput_device_get_user_data(libinput_event_get_device(event)); + touch_event = libinput_event_get_touch_event(event); + timestamp = libinput_event_touch_get_time_usec(touch_event); + + // get the multitouch slot for this event + // can return -1 when the device is a single touch device + slot = libinput_event_touch_get_slot(touch_event); + if (slot == -1) { + slot = 0; + } + + device_id = data->flutter_device_id_offset + slot; + + emit_pointer_events(input, &FLUTTER_POINTER_TOUCH_UP_EVENT(timestamp, data->x, data->y, device_id), 1); + + return 0; +} + +static int on_touch_motion(struct user_input *input, struct libinput_event *event) { + struct libinput_event_touch *touch_event; + struct input_device_data *data; + uint64_t timestamp; + int64_t device_id; + double x, y; + int slot; + + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + + data = libinput_device_get_user_data(libinput_event_get_device(event)); + touch_event = libinput_event_get_touch_event(event); + timestamp = libinput_event_touch_get_time_usec(touch_event); + + // get the multitouch slot for this event + // can return -1 when the device is a single touch device + slot = libinput_event_touch_get_slot(touch_event); + if (slot == -1) { + slot = 0; + } + + device_id = data->flutter_device_id_offset + slot; + + x = libinput_event_touch_get_x_transformed(touch_event, input->display_width - 1); + y = libinput_event_touch_get_y_transformed(touch_event, input->display_height - 1); + + // transform the display coordinates to view (flutter) coordinates + apply_flutter_transformation(input->display_to_view_transform, &x, &y); + + // emit the flutter pointer event + emit_pointer_events(input, &FLUTTER_POINTER_TOUCH_MOVE_EVENT(timestamp, x, y, device_id), 1); + + // alter our device state + data->x = x; + data->y = y; + data->timestamp = timestamp; + + return 0; +} + +static int on_touch_cancel(struct user_input *input, struct libinput_event *event) { + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + /// TODO: Implement touch cancel + return 0; +} + +static int on_touch_frame(struct user_input *input, struct libinput_event *event) { + DEBUG_ASSERT(input != NULL); + DEBUG_ASSERT(event != NULL); + /// TODO: Implement touch frame + return 0; +} + + +static int process_libinput_events(struct user_input *input, uint64_t timestamp) { + enum libinput_event_type event_type; + struct libinput_event *event; + int ok; + + DEBUG_ASSERT(input != NULL); + + while (libinput_next_event_type(input->libinput) != LIBINPUT_EVENT_NONE) { + event = libinput_get_event(input->libinput); + event_type = libinput_event_get_type(event); + + switch (event_type) { + case LIBINPUT_EVENT_DEVICE_ADDED: + ok = on_device_added(input, event, timestamp); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_DEVICE_REMOVED: + ok = on_device_removed(input, event, timestamp); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_KEYBOARD_KEY: + ok = on_key_event(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_POINTER_MOTION: + ok = on_mouse_motion_event(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE: + ok = on_mouse_motion_absolute_event(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_POINTER_BUTTON: + ok = on_mouse_button_event(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_POINTER_AXIS: + ok = on_mouse_axis_event(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TOUCH_DOWN: + ok = on_touch_down(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TOUCH_UP: + ok = on_touch_up(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TOUCH_MOTION: + ok = on_touch_motion(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TOUCH_CANCEL: + ok = on_touch_cancel(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + case LIBINPUT_EVENT_TOUCH_FRAME: + ok = on_touch_frame(input, event); + if (ok != 0) { + goto fail_destroy_event; + } + break; + default: + break; + } + + libinput_event_destroy(event); + } + + return 0; + + + fail_destroy_event: + libinput_event_destroy(event); + + fail_return_ok: + return ok; +} + +int user_input_on_fd_ready(struct user_input *input) { + unsigned int cursor_x, cursor_y, cursor_x_before, cursor_y_before; + uint64_t timestamp; + bool cursor_enabled, cursor_enabled_before; + int ok; + + DEBUG_ASSERT(input != NULL); + + // get a timestamp because some libinput events don't provide one + // needs to be in milliseconds, since that's what the other libinput events + // use and what flutter pointer events require + timestamp = get_monotonic_time() / 1000; + + // tell libinput about new events + ok = libinput_dispatch(input->libinput); + if (ok < 0) { + LOG_USER_INPUT_ERROR("Could not notify libinput about new input events. libinput_dispatch: %s\n", strerror(-ok)); + return -ok; + } + + // record cursor state before handling events + cursor_enabled_before = input->n_cursor_devices > 0; + cursor_x_before = (unsigned int) round(input->cursor_x); + cursor_y_before = (unsigned int) round(input->cursor_y); + + // handle all available libinput events + ok = process_libinput_events(input, timestamp); + if (ok != 0) { + LOG_USER_INPUT_ERROR("Could not process libinput events. process_libinput_events: %s\n", strerror(ok)); + return ok; + } + + // record cursor state after handling events + cursor_enabled = input->n_cursor_devices > 0; + cursor_x = (unsigned int) round(input->cursor_x); + cursor_y = (unsigned int) round(input->cursor_y); + + // make sure we've dispatched all the flutter pointer events + flush_pointer_events(input); + + // call the interface callback if the cursor has been enabled or disabled + if (cursor_enabled && !cursor_enabled_before) { + input->interface.on_set_cursor_enabled(input->userdata, true); + } else if (!cursor_enabled && cursor_enabled_before) { + input->interface.on_set_cursor_enabled(input->userdata, false); + } + + // only move the pointer if the cursor is enabled now + if (cursor_enabled && ((cursor_x != cursor_x_before) || (cursor_y != cursor_y_before))) { + input->interface.on_move_cursor(input->userdata, cursor_x, cursor_y); + } + + return 0; +}