diff --git a/CMakeLists.txt b/CMakeLists.txt index e5ea7082..1a84af76 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,8 @@ pkg_check_modules(GBM REQUIRED gbm) pkg_check_modules(EGL REQUIRED egl) pkg_check_modules(GLESV2 REQUIRED glesv2) pkg_check_modules(LIBSYSTEMD REQUIRED libsystemd) -pkg_check_modules(LIBINPUT libinput) +pkg_check_modules(LIBINPUT REQUIRED libinput) +pkg_check_modules(LIBXKBCOMMON REQUIRED xkbcommon) pkg_check_modules(LIBUDEV libudev) pkg_check_modules(GPIOD libgpiod) @@ -99,12 +100,12 @@ set(FLUTTER_PI_SRC src/flutter-pi.c src/platformchannel.c src/pluginregistry.c - src/console_keyboard.c src/texture_registry.c src/compositor.c src/modesetting.c src/collection.c - src/cursor.c + src/cursor.c + src/keyboard.c src/plugins/services.c src/plugins/testplugin.c src/plugins/text_input.c @@ -136,6 +137,7 @@ target_link_libraries(flutter-pi ${LIBINPUT_LDFLAGS} ${LIBUDEV_LDFLAGS} ${GPIOD_LDFLAGS} + ${LIBXKBCOMMON_LDFLAGS} pthread dl rt m ) @@ -151,6 +153,7 @@ target_include_directories(flutter-pi PRIVATE ${LIBINPUT_INCLUDE_DIRS} ${LIBUDEV_INCLUDE_DIRS} ${GPIOD_INCLUDE_DIRS} + ${LIBXKBCOMMON_INCLUDE_DIRS} ) target_compile_options(flutter-pi PRIVATE @@ -162,6 +165,7 @@ target_compile_options(flutter-pi PRIVATE ${LIBINPUT_CFLAGS} ${LIBUDEV_CFLAGS} ${GPIOD_CFLAGS} + ${LIBXKBCOMMON_CFLAGS} -ggdb -DBUILD_TEXT_INPUT_PLUGIN -DBUILD_SPIDEV_PLUGIN diff --git a/Makefile b/Makefile index 9a569e0e..948e172c 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -REAL_CFLAGS = -I./include $(shell pkg-config --cflags gbm libdrm glesv2 egl libsystemd libinput libudev) \ +REAL_CFLAGS = -I./include $(shell pkg-config --cflags gbm libdrm glesv2 egl libsystemd libinput libudev xkbcommon) \ -DBUILD_TEXT_INPUT_PLUGIN \ -DBUILD_TEST_PLUGIN \ -DBUILD_OMXPLAYER_VIDEO_PLAYER_PLUGIN \ @@ -6,7 +6,7 @@ REAL_CFLAGS = -I./include $(shell pkg-config --cflags gbm libdrm glesv2 egl libs $(CFLAGS) REAL_LDFLAGS = \ - $(shell pkg-config --libs gbm libdrm glesv2 egl libsystemd libinput libudev) \ + $(shell pkg-config --libs gbm libdrm glesv2 egl libsystemd libinput libudev xkbcommon) \ -lrt \ -lpthread \ -ldl \ @@ -17,12 +17,12 @@ REAL_LDFLAGS = \ SOURCES = src/flutter-pi.c \ src/platformchannel.c \ src/pluginregistry.c \ - src/console_keyboard.c \ src/texture_registry.c \ src/compositor.c \ src/modesetting.c \ src/collection.c \ src/cursor.c \ + src/keyboard.c \ src/plugins/services.c \ src/plugins/testplugin.c \ src/plugins/text_input.c \ diff --git a/README.md b/README.md index 08e42dfe..65767476 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ ## 📰 NEWS -- the physical dimensions of the screen can now be specified via cmdline, using the `--dimensions` option. -- the layout of the engine-binaries branch has changed again. The symbolic link from `libflutter_engine.so` to the fitting `libflutter_engine.so.release` or `libflutter_engine.so.debug` is no longer needed, flutter-pi will now dynamically load the engine fitting the the runtime mode that was specified via cmdline. (if `--release` is given, flutter-pi will load `libflutter_engine.so.release`, else `libflutter_engine.so.debug`) -- flutter-pi now requires `libsystemd-dev`, `libinput-dev` and `libudev-dev` at compile-time. (`libudev-dev` is actually optional. To build without udev support, use cmake.) -- flutter-pi and the engine binaries updated for flutter 1.20. -- it's possible to run flutter-pi in AOT mode now. Instructions for that are WIP. -- `--aot` was renamed to `--release` +- flutter-pi now requires `libxkbcommon`. Install using `sudo apt install libxkbcommon-dev` +- keyboard input works better now. You can now use any keyboard connected to the Raspberry Pi for text and raw keyboard input. # flutter-pi A light-weight Flutter Engine Embedder for Raspberry Pi. Inspired by https://github.com/chinmaygarde/flutter_from_scratch. @@ -236,7 +232,7 @@ sudo fc-cache ``` ### libgpiod (for the included GPIO plugin), libsystemd, libinput, libudev ```bash -sudo apt-get install gpiod libgpiod-dev libsystemd-dev libinput-dev libudev-dev +sudo apt-get install gpiod libgpiod-dev libsystemd-dev libinput-dev libudev-dev libxkbcommon-dev ``` ## Compiling flutter-pi (on the Raspberry Pi) @@ -250,13 +246,6 @@ The _flutter-pi_ executable will then be located at this path: `/path/to/the/clo ## Performance Performance is actually better than I expected. With most of the apps inside the `flutter SDK -> examples -> catalog` directory I get smooth 50-60fps. -## Keyboard Input -Keyboard input is supported. **There is one important limitation though**. Text input (i.e. writing any kind of text/symbols to flutter input fields) only works when typing on the keyboard, which is attached to the terminal flutter-pi is running on. So, if you ssh into your Raspberry Pi to run flutter-pi, you have to enter text into your ssh terminal. - -Raw Keyboard input (i.e. using tab to iterate through focus nodes) works with any keyboard attached to your Raspberry Pi. - -converting raw key-codes to text symbols is not that easy (because of all the different keyboard layouts), so for text input flutter-pi basically uses `stdin`. - ## Touchscreen Latency Due to the way the touchscreen driver works in raspbian, there's some delta between an actual touch of the touchscreen and a touch event arriving at userspace. The touchscreen driver in the raspbian kernel actually just repeatedly polls some buffer shared with the firmware running on the VideoCore, and the videocore repeatedly polls the touchscreen. (both at 60Hz) So on average, there's a delay of 17ms (minimum 0ms, maximum 34ms). If I have enough time in the future, I'll try to build a better touchscreen driver to lower the delay. diff --git a/include/console_keyboard.h b/include/console_keyboard.h deleted file mode 100644 index b3b70f41..00000000 --- a/include/console_keyboard.h +++ /dev/null @@ -1,191 +0,0 @@ -#ifndef _CONSOLE_KEYBOARD_H -#define _CONSOLE_KEYBOARD_H - -#include -#include - -// small subset of the GLFW key ids. -// (only the ones needed for text input) - -typedef enum { - GLFW_KEY_UNKNOWN = -1, - GLFW_KEY_SPACE = 32, - GLFW_KEY_APOSTROPHE = 39, - GLFW_KEY_COMMA = 44, - GLFW_KEY_MINUS = 45, - GLFW_KEY_PERIOD = 46, - GLFW_KEY_SLASH = 47, - GLFW_KEY_0 = 48, - GLFW_KEY_1 = 49, - GLFW_KEY_2 = 50, - GLFW_KEY_3 = 51, - GLFW_KEY_4 = 52, - GLFW_KEY_5 = 53, - GLFW_KEY_6 = 54, - GLFW_KEY_7 = 55, - GLFW_KEY_8 = 56, - GLFW_KEY_9 = 57, - GLFW_KEY_SEMICOLON = 59, - GLFW_KEY_EQUAL = 61, - GLFW_KEY_A = 65, - GLFW_KEY_B = 66, - GLFW_KEY_C = 67, - GLFW_KEY_D = 68, - GLFW_KEY_E = 69, - GLFW_KEY_F = 70, - GLFW_KEY_G = 71, - GLFW_KEY_H = 72, - GLFW_KEY_I = 73, - GLFW_KEY_J = 74, - GLFW_KEY_K = 75, - GLFW_KEY_L = 76, - GLFW_KEY_M = 77, - GLFW_KEY_N = 78, - GLFW_KEY_O = 79, - GLFW_KEY_P = 80, - GLFW_KEY_Q = 81, - GLFW_KEY_R = 82, - GLFW_KEY_S = 83, - GLFW_KEY_T = 84, - GLFW_KEY_U = 85, - GLFW_KEY_V = 86, - GLFW_KEY_W = 87, - GLFW_KEY_X = 88, - GLFW_KEY_Y = 89, - GLFW_KEY_Z = 90, - GLFW_KEY_LEFT_BRACKET = 91, - GLFW_KEY_BACKSLASH = 92, - GLFW_KEY_RIGHT_BRACKET = 93, - GLFW_KEY_GRAVE_ACCENT = 96, - GLFW_KEY_WORLD_1 = 161, - GLFW_KEY_WORLD_2 = 162, - GLFW_KEY_ESCAPE = 256, - GLFW_KEY_ENTER = 257, - GLFW_KEY_TAB = 258, - GLFW_KEY_BACKSPACE = 259, - GLFW_KEY_INSERT = 260, - GLFW_KEY_DELETE = 261, - GLFW_KEY_RIGHT = 262, - GLFW_KEY_LEFT = 263, - GLFW_KEY_DOWN = 264, - GLFW_KEY_UP = 265, - GLFW_KEY_PAGE_UP = 266, - GLFW_KEY_PAGE_DOWN = 267, - GLFW_KEY_HOME = 268, - GLFW_KEY_END = 269, - GLFW_KEY_CAPS_LOCK = 280, - GLFW_KEY_SCROLL_LOCK = 281, - GLFW_KEY_NUM_LOCK = 282, - GLFW_KEY_PRINT_SCREEN = 283, - GLFW_KEY_PAUSE = 284, - GLFW_KEY_F1 = 290, - GLFW_KEY_F2 = 291, - GLFW_KEY_F3 = 292, - GLFW_KEY_F4 = 293, - GLFW_KEY_F5 = 294, - GLFW_KEY_F6 = 295, - GLFW_KEY_F7 = 296, - GLFW_KEY_F8 = 297, - GLFW_KEY_F9 = 298, - GLFW_KEY_F10 = 299, - GLFW_KEY_F11 = 300, - GLFW_KEY_F12 = 301, - GLFW_KEY_F13 = 302, - GLFW_KEY_F14 = 303, - GLFW_KEY_F15 = 304, - GLFW_KEY_F16 = 305, - GLFW_KEY_F17 = 306, - GLFW_KEY_F18 = 307, - GLFW_KEY_F19 = 308, - GLFW_KEY_F20 = 309, - GLFW_KEY_F21 = 310, - GLFW_KEY_F22 = 311, - GLFW_KEY_F23 = 312, - GLFW_KEY_F24 = 313, - GLFW_KEY_F25 = 314, - GLFW_KEY_KP_0 = 320, - GLFW_KEY_KP_1 = 321, - GLFW_KEY_KP_2 = 322, - GLFW_KEY_KP_3 = 323, - GLFW_KEY_KP_4 = 324, - GLFW_KEY_KP_5 = 325, - GLFW_KEY_KP_6 = 326, - GLFW_KEY_KP_7 = 327, - GLFW_KEY_KP_8 = 328, - GLFW_KEY_KP_9 = 329, - GLFW_KEY_KP_DECIMAL = 330, - GLFW_KEY_KP_DIVIDE = 331, - GLFW_KEY_KP_MULTIPLY = 332, - GLFW_KEY_KP_SUBTRACT = 333, - GLFW_KEY_KP_ADD = 334, - GLFW_KEY_KP_ENTER = 335, - GLFW_KEY_KP_EQUAL = 336, - GLFW_KEY_LEFT_SHIFT = 340, - GLFW_KEY_LEFT_CONTROL = 341, - GLFW_KEY_LEFT_ALT = 342, - GLFW_KEY_LEFT_SUPER = 343, - GLFW_KEY_RIGHT_SHIFT = 344, - GLFW_KEY_RIGHT_CONTROL = 345, - GLFW_KEY_RIGHT_ALT = 346, - GLFW_KEY_RIGHT_SUPER = 347, - GLFW_KEY_MENU = 348, -} glfw_key; - -#define GLFW_KEY_LAST 348 - -typedef enum { - GLFW_RELEASE = 0, - GLFW_PRESS = 1, - GLFW_REPEAT = 2 -} glfw_key_action; - -typedef enum { - GLFW_MOD_SHIFT = 1, - GLFW_MOD_CONTROL = 2, - GLFW_MOD_ALT = 4, - GLFW_MOD_SUPER = 8, - GLFW_MOD_CAPS_LOCK = 16, - GLFW_MOD_NUM_LOCK = 32 -} glfw_keymod; - -#define GLFW_KEYMOD_FOR_KEY(keycode) \ - (((keycode == GLFW_KEY_LEFT_SHIFT) || (keycode == GLFW_KEY_RIGHT_SHIFT)) ? GLFW_MOD_SHIFT : \ - ((keycode == GLFW_KEY_LEFT_CONTROL) || (keycode == GLFW_KEY_RIGHT_CONTROL)) ? GLFW_MOD_CONTROL : \ - ((keycode == GLFW_KEY_LEFT_ALT) || (keycode == GLFW_KEY_RIGHT_ALT)) ? GLFW_MOD_ALT : \ - ((keycode == GLFW_KEY_LEFT_SUPER) || (keycode == GLFW_KEY_RIGHT_SUPER)) ? GLFW_MOD_SUPER : \ - (keycode == GLFW_KEY_CAPS_LOCK) ? GLFW_MOD_CAPS_LOCK : \ - (keycode == GLFW_KEY_NUM_LOCK) ? GLFW_MOD_NUM_LOCK : 0); - -#define GLFW_KEY_IS_RIGHTSIDED(keycode) \ - ((keycode == GLFW_KEY_RIGHT_SHIFT) ? true : \ - (keycode == GLFW_KEY_RIGHT_CONTROL) ? true : \ - (keycode == GLFW_KEY_RIGHT_ALT) ? true : \ - (keycode == GLFW_KEY_RIGHT_SUPER) ? true : false) - -typedef uint8_t glfw_keymod_map; - -extern char *glfw_key_control_sequence[GLFW_KEY_LAST+1]; -extern glfw_key evdev_code_glfw_key[KEY_CNT]; - -#define EVDEV_KEY_TO_GLFW_KEY(key) evdev_code_glfw_key[key] - - -int console_flush_stdin(void); -int console_make_raw(void); -int console_restore(void); - -/// tries to parse the console input represented by the string `input` -/// as a keycode () -size_t utf8_symbol_length(char *c); - -static inline char *utf8_symbol_at(char *utf8str, unsigned int symbol_index) { - for (; symbol_index && *utf8str; symbol_index--) - utf8str += utf8_symbol_length(utf8str); - - return symbol_index? NULL : utf8str; -} - -glfw_key console_try_get_key(char *input, char **input_out); -char *console_try_get_utf8char(char *input, char **input_out); - -#endif \ No newline at end of file diff --git a/include/flutter-pi.h b/include/flutter-pi.h index 60f8b212..7820f23c 100644 --- a/include/flutter-pi.h +++ b/include/flutter-pi.h @@ -26,6 +26,7 @@ #include #include +#include #define LOAD_EGL_PROC(flutterpi_struct, name) \ do { \ @@ -394,13 +395,15 @@ struct flutterpi { 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; - sd_event_source *stdin_event_source; + struct keyboard_config *keyboard_config; + int64_t next_unused_flutter_device_id; double cursor_x, cursor_y; } input; @@ -452,6 +455,7 @@ 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; diff --git a/include/keyboard.h b/include/keyboard.h new file mode 100644 index 00000000..16eb3a8a --- /dev/null +++ b/include/keyboard.h @@ -0,0 +1,120 @@ +#ifndef KEYBOARD_H +#define KEYBOARD_H + +#include + +struct keyboard_config { + struct xkb_context *context; + struct xkb_keymap *default_keymap; + struct xkb_compose_table *default_compose_table; +}; + +struct keyboard_state { + struct keyboard_config *config; + struct xkb_state *state; + struct xkb_state *plain_state; + struct xkb_compose_state *compose_state; + int n_iso_level2; + int n_iso_level3; + int n_iso_level5; +}; + +struct keyboard_modifier_state { + bool ctrl:1; + bool shift:1; + bool alt:1; + bool meta:1; + bool capslock:1; + bool numlock:1; + bool scrolllock:1; +}; + +#define KEY_RELEASE 0 +#define KEY_PRESS 1 +#define KEY_REPEAT 2 + +struct keyboard_config *keyboard_config_new(void); + +void keyboard_config_destroy(struct keyboard_config *config); + +struct keyboard_state *keyboard_state_new( + struct keyboard_config *config, + struct xkb_keymap *keymap_override, + struct xkb_compose_table *compose_table_override +); + +void keyboard_state_destroy( + struct keyboard_state *state +); + +int keyboard_state_process_key_event( + struct keyboard_state *state, + uint16_t evdev_keycode, + int32_t evdev_value, + xkb_keysym_t *keysym_out, + uint32_t *codepoint_out +); + +uint32_t keyboard_state_get_plain_codepoint( + struct keyboard_state *state, + uint16_t evdev_keycode, + int32_t evdev_value +); + +static inline bool keyboard_state_is_ctrl_active( + struct keyboard_state *state +) { + return xkb_state_mod_name_is_active(state->state, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE); +} + +static inline bool keyboard_state_is_shift_active( + struct keyboard_state *state +) { + return xkb_state_mod_name_is_active(state->state, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE); +} + +static inline bool keyboard_state_is_alt_active( + struct keyboard_state *state +) { + return xkb_state_mod_name_is_active(state->state, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE); +} + +static inline bool keyboard_state_is_meta_active( + struct keyboard_state *state +) { + return xkb_state_mod_name_is_active(state->state, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE); +} + +static inline bool keyboard_state_is_capslock_active( + struct keyboard_state *state +) { + return xkb_state_mod_name_is_active(state->state, XKB_MOD_NAME_CAPS, XKB_STATE_MODS_EFFECTIVE); +} + +static inline bool keyboard_state_is_numlock_active( + struct keyboard_state *state +) { + return xkb_state_mod_name_is_active(state->state, XKB_MOD_NAME_NUM, XKB_STATE_MODS_EFFECTIVE); +} + +static inline bool keyboard_state_is_scrolllock_active( + struct keyboard_state *state +) { + return xkb_state_mod_name_is_active(state->state, "Mod3", XKB_STATE_MODS_EFFECTIVE); +} + +static inline struct keyboard_modifier_state keyboard_state_get_meta_state( + struct keyboard_state *state +) { + return (struct keyboard_modifier_state) { + .ctrl = keyboard_state_is_ctrl_active(state), + .shift = keyboard_state_is_shift_active(state), + .alt = keyboard_state_is_alt_active(state), + .meta = keyboard_state_is_meta_active(state), + .capslock = keyboard_state_is_capslock_active(state), + .numlock = keyboard_state_is_numlock_active(state), + .scrolllock = keyboard_state_is_scrolllock_active(state) + }; +} + +#endif \ No newline at end of file diff --git a/include/plugins/raw_keyboard.h b/include/plugins/raw_keyboard.h index b552f9e1..75585f78 100644 --- a/include/plugins/raw_keyboard.h +++ b/include/plugins/raw_keyboard.h @@ -1,11 +1,34 @@ #ifndef _KEY_EVENT_H #define _KEY_EVENT_H +#include +#include + #define KEY_EVENT_CHANNEL "flutter/keyevent" -#include +int rawkb_send_android_keyevent( + uint32_t flags, + uint32_t code_point, + unsigned int key_code, + uint32_t plain_code_point, + uint32_t scan_code, + uint32_t meta_state, + uint32_t source, + uint16_t vendor_id, + uint16_t product_id, + uint16_t device_id, + int repeat_count, + bool is_down, + char *character +); -int rawkb_on_keyevent(glfw_key key, uint32_t scan_code, glfw_key_action action); +int rawkb_send_gtk_keyevent( + uint32_t unicode_scalar_values, + uint32_t key_code, + uint32_t scan_code, + uint32_t modifiers, + bool is_down +); int rawkb_init(void); int rawkb_deinit(void); diff --git a/include/plugins/text_input.h b/include/plugins/text_input.h index d6e8f849..21bf8d91 100644 --- a/include/plugins/text_input.h +++ b/include/plugins/text_input.h @@ -1,7 +1,7 @@ #ifndef _TEXT_INPUT_H #define _TEXT_INPUT_H -#include +#include #define TEXT_INPUT_CHANNEL "flutter/textinput" @@ -16,6 +16,8 @@ enum text_input_type { kInputTypeEmailAddress, kInputTypeUrl, kInputTypeVisiblePassword, + kInputTypeName, + kInputTypeAddress }; enum text_input_action { @@ -40,24 +42,16 @@ struct text_input_configuration { enum text_input_action input_action; }; -int textin_sync_editing_state(void); -int textin_perform_action(enum text_input_action action); -int textin_on_connection_closed(void); - -// TextInput model functions (updating the text editing state) -bool textin_delete_selected(void); -bool textin_add_utf8_char(char *c); -bool textin_backspace(void); -bool textin_delete(void); -bool textin_move_cursor_to_beginning(void); -bool textin_move_cursor_to_end(void); -bool textin_move_cursor_forward(void); -bool textin_move_cursor_back(void); +enum floating_cursor_drag_state { + kFloatingCursorDragStateStart, + kFloatingCursorDragStateUpdate, + kFloatingCursorDragStateEnd +}; // parses the input string as linux terminal input and calls the TextInput model functions // accordingly. -int textin_on_utf8_char(char *c); -int textin_on_key(glfw_key key); +int textin_on_utf8_char(uint8_t *c); +int textin_on_xkb_keysym(xkb_keysym_t keysym); int textin_init(void); int textin_deinit(void); diff --git a/src/console_keyboard.c b/src/console_keyboard.c deleted file mode 100644 index c7da6d87..00000000 --- a/src/console_keyboard.c +++ /dev/null @@ -1,279 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -char *glfw_key_control_sequence[GLFW_KEY_LAST+1] = { - NULL, - [GLFW_KEY_ENTER] = "\n", - [GLFW_KEY_TAB] = "\t", - [GLFW_KEY_BACKSPACE] = "\x7f", - NULL, - [GLFW_KEY_DELETE] = "\e[3~", - [GLFW_KEY_RIGHT] = "\e[C", - [GLFW_KEY_LEFT] = "\e[D", - NULL, - [GLFW_KEY_PAGE_UP] = "\e[5~", - [GLFW_KEY_PAGE_DOWN] = "\e[6~", - [GLFW_KEY_HOME] = "\e[1~", - [GLFW_KEY_END] = "\e[4~", - NULL, - - // function keys - [GLFW_KEY_F1] = "\eOP", "\eOQ", "\eOR", "\eOS", - "\e[15~", "\e[17~", "\e[18~", "\e[19~", - "\e[20~", "\e[21~", "\e[23~", "\e[24~" -}; - -#define EVDEV_TO_GLFW(keyname) [KEY_##keyname] = GLFW_KEY_##keyname -#define EVDEV_TO_GLFW_RENAME(linux_keyname, glfw_keyname) [KEY_##linux_keyname] = GLFW_KEY_##glfw_keyname - -glfw_key evdev_code_glfw_key[KEY_CNT] = { - EVDEV_TO_GLFW(SPACE), - EVDEV_TO_GLFW(APOSTROPHE), - EVDEV_TO_GLFW(COMMA), - EVDEV_TO_GLFW(MINUS), - EVDEV_TO_GLFW_RENAME(DOT, PERIOD), - EVDEV_TO_GLFW(SLASH), - EVDEV_TO_GLFW(0), - EVDEV_TO_GLFW(1), - EVDEV_TO_GLFW(2), - EVDEV_TO_GLFW(3), - EVDEV_TO_GLFW(4), - EVDEV_TO_GLFW(5), - EVDEV_TO_GLFW(6), - EVDEV_TO_GLFW(7), - EVDEV_TO_GLFW(8), - EVDEV_TO_GLFW(9), - EVDEV_TO_GLFW(SEMICOLON), - EVDEV_TO_GLFW(EQUAL), - EVDEV_TO_GLFW(A), - EVDEV_TO_GLFW(B), - EVDEV_TO_GLFW(C), - EVDEV_TO_GLFW(D), - EVDEV_TO_GLFW(E), - EVDEV_TO_GLFW(F), - EVDEV_TO_GLFW(G), - EVDEV_TO_GLFW(H), - EVDEV_TO_GLFW(I), - EVDEV_TO_GLFW(J), - EVDEV_TO_GLFW(K), - EVDEV_TO_GLFW(L), - EVDEV_TO_GLFW(M), - EVDEV_TO_GLFW(N), - EVDEV_TO_GLFW(O), - EVDEV_TO_GLFW(P), - EVDEV_TO_GLFW(Q), - EVDEV_TO_GLFW(R), - EVDEV_TO_GLFW(S), - EVDEV_TO_GLFW(T), - EVDEV_TO_GLFW(U), - EVDEV_TO_GLFW(V), - EVDEV_TO_GLFW(W), - EVDEV_TO_GLFW(X), - EVDEV_TO_GLFW(Y), - EVDEV_TO_GLFW(Z), - EVDEV_TO_GLFW_RENAME(LEFTBRACE, LEFT_BRACKET), - EVDEV_TO_GLFW(BACKSLASH), - EVDEV_TO_GLFW_RENAME(RIGHTBRACE, RIGHT_BRACKET), - EVDEV_TO_GLFW_RENAME(GRAVE, GRAVE_ACCENT), - EVDEV_TO_GLFW_RENAME(ESC, ESCAPE), - EVDEV_TO_GLFW(ENTER), - EVDEV_TO_GLFW(TAB), - EVDEV_TO_GLFW(BACKSPACE), - EVDEV_TO_GLFW(INSERT), - EVDEV_TO_GLFW(DELETE), - EVDEV_TO_GLFW(RIGHT), - EVDEV_TO_GLFW(LEFT), - EVDEV_TO_GLFW(DOWN), - EVDEV_TO_GLFW(UP), - EVDEV_TO_GLFW_RENAME(PAGEUP, PAGE_UP), - EVDEV_TO_GLFW_RENAME(PAGEDOWN, PAGE_DOWN), - EVDEV_TO_GLFW(HOME), - EVDEV_TO_GLFW(END), - EVDEV_TO_GLFW_RENAME(CAPSLOCK, CAPS_LOCK), - EVDEV_TO_GLFW_RENAME(SCROLLLOCK, SCROLL_LOCK), - EVDEV_TO_GLFW_RENAME(NUMLOCK, NUM_LOCK), - EVDEV_TO_GLFW_RENAME(SYSRQ, PRINT_SCREEN), - EVDEV_TO_GLFW(PAUSE), - EVDEV_TO_GLFW(F1), - EVDEV_TO_GLFW(F2), - EVDEV_TO_GLFW(F3), - EVDEV_TO_GLFW(F4), - EVDEV_TO_GLFW(F5), - EVDEV_TO_GLFW(F6), - EVDEV_TO_GLFW(F7), - EVDEV_TO_GLFW(F8), - EVDEV_TO_GLFW(F9), - EVDEV_TO_GLFW(F10), - EVDEV_TO_GLFW(F11), - EVDEV_TO_GLFW(F12), - EVDEV_TO_GLFW(F13), - EVDEV_TO_GLFW(F14), - EVDEV_TO_GLFW(F15), - EVDEV_TO_GLFW(F16), - EVDEV_TO_GLFW(F17), - EVDEV_TO_GLFW(F18), - EVDEV_TO_GLFW(F19), - EVDEV_TO_GLFW(F20), - EVDEV_TO_GLFW(F21), - EVDEV_TO_GLFW(F22), - EVDEV_TO_GLFW(F23), - EVDEV_TO_GLFW(F24), - EVDEV_TO_GLFW_RENAME(KP0, KP_0), - EVDEV_TO_GLFW_RENAME(KP1, KP_1), - EVDEV_TO_GLFW_RENAME(KP2, KP_2), - EVDEV_TO_GLFW_RENAME(KP3, KP_3), - EVDEV_TO_GLFW_RENAME(KP4, KP_4), - EVDEV_TO_GLFW_RENAME(KP5, KP_5), - EVDEV_TO_GLFW_RENAME(KP6, KP_6), - EVDEV_TO_GLFW_RENAME(KP7, KP_7), - EVDEV_TO_GLFW_RENAME(KP8, KP_8), - EVDEV_TO_GLFW_RENAME(KP9, KP_9), - EVDEV_TO_GLFW_RENAME(KPDOT, KP_DECIMAL), - EVDEV_TO_GLFW_RENAME(KPSLASH, KP_DIVIDE), - EVDEV_TO_GLFW_RENAME(KPASTERISK, KP_MULTIPLY), - EVDEV_TO_GLFW_RENAME(KPMINUS, KP_SUBTRACT), - EVDEV_TO_GLFW_RENAME(KPPLUS, KP_ADD), - EVDEV_TO_GLFW_RENAME(KPENTER, KP_ENTER), - //CONVERSION(KP_EQUAL), // what is the equivalent of KP_EQUAL? is it - EVDEV_TO_GLFW_RENAME(LEFTSHIFT, LEFT_SHIFT), - EVDEV_TO_GLFW_RENAME(LEFTCTRL, LEFT_CONTROL), - EVDEV_TO_GLFW_RENAME(LEFTALT, LEFT_ALT), - EVDEV_TO_GLFW_RENAME(LEFTMETA, LEFT_SUPER), - EVDEV_TO_GLFW_RENAME(RIGHTSHIFT, RIGHT_SHIFT), - EVDEV_TO_GLFW_RENAME(RIGHTCTRL, RIGHT_CONTROL), - EVDEV_TO_GLFW_RENAME(RIGHTALT, RIGHT_ALT), - EVDEV_TO_GLFW_RENAME(RIGHTMETA, RIGHT_SUPER), - EVDEV_TO_GLFW(MENU), // could also be that the linux equivalent is KEY_COMPOSE -}; - -#undef EVDEV_TO_GLFW -#undef EVDEV_TO_GLFW_RENAME - -int console_flush_stdin(void) { - int ok; - - ok = tcflush(STDIN_FILENO, TCIFLUSH); - if (ok == -1) { - perror("could not flush stdin"); - return errno; - } - - return 0; -} - -struct termios original_config; -bool is_raw = false; - -int console_make_raw(void) { - struct termios config; - int ok; - - if (is_raw) return 0; - - ok = tcgetattr(STDIN_FILENO, &config); - if (ok == -1) { - perror("could not get terminal attributes"); - return errno; - } - - original_config = config; - - config.c_lflag &= ~(ECHO | ICANON); - - //config.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON); - //config.c_oflag &= ~OPOST; - //config.c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN); - //config.c_cflag &= ~(CSIZE | PARENB); - //config.c_cflag |= CS8; - - ok = tcsetattr(STDIN_FILENO, TCSANOW, &config); - if (ok == -1) { - perror("could not set terminal attributes"); - return errno; - } - - return 0; -} - -int console_restore(void) { - int ok; - - if (!is_raw) return 0; - - ok = tcsetattr(STDIN_FILENO, TCSANOW, &original_config); - if (ok == -1) { - perror("could not set terminal attributes"); - return errno; - } - - is_raw = false; - - return 0; -} - -size_t utf8_symbol_length(char *c) { - uint8_t first = ((uint8_t*) c)[0]; - uint8_t second = ((uint8_t*) c)[1]; - uint8_t third = ((uint8_t*) c)[2]; - uint8_t fourth = ((uint8_t*) c)[3]; - - if (first <= 0b01111111) { - // ASCII - return 1; - } else if (((first >> 5) == 0b110) && ((second >> 6) == 0b10)) { - // 2-byte UTF8 - return 2; - } else if (((first >> 4) == 0b1110) && ((second >> 6) == 0b10) && ((third >> 6) == 0b10)) { - // 3-byte UTF8 - return 3; - } else if (((first >> 3) == 0b11110) && ((second >> 6) == 0b10) && ((third >> 6) == 0b10) && ((fourth >> 6) == 0b10)) { - // 4-byte UTF8 - return 4; - } - - return 0; -} - -glfw_key console_try_get_key(char *input, char **input_out) { - if (input_out) - *input_out = input; - - for (glfw_key key = 0; key <= GLFW_KEY_LAST; key++) { - if (glfw_key_control_sequence[key] == NULL) - continue; - - if (strcmp(input, glfw_key_control_sequence[key]) == 0) { - if (input_out) - *input_out += strlen(glfw_key_control_sequence[key]); - - return key; - } - } - - return GLFW_KEY_UNKNOWN; -} - -char *console_try_get_utf8char(char *input, char **input_out) { - if (input_out) - *input_out = input; - - size_t length = utf8_symbol_length(input); - - if ((length == 1) && !isprint(*input)) - return NULL; - - if (length == 0) - return NULL; - - *input_out += length; - - return input; -} \ No newline at end of file diff --git a/src/flutter-pi.c b/src/flutter-pi.c index dfb3e969..26640b04 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -44,7 +44,7 @@ #include #include -#include +#include #include #include #include @@ -1890,6 +1890,12 @@ static void libinput_interface_on_close(int fd, void *userdata) { } static int on_libinput_ready(sd_event_source *s, int fd, uint32_t revents, void *userdata) { + struct libinput_event_keyboard *keyboard_event; + struct libinput_event_pointer *pointer_event; + struct libinput_event_touch *touch_event; + struct input_device_data *data; + enum libinput_event_type type; + struct libinput_device *device; struct libinput_event *event; FlutterPointerEvent pointer_events[64]; FlutterEngineResult result; @@ -1903,12 +1909,12 @@ static int on_libinput_ready(sd_event_source *s, int fd, uint32_t revents, void } while (event = libinput_get_event(flutterpi.input.libinput), event != NULL) { - enum libinput_event_type type = libinput_event_get_type(event); + type = libinput_event_get_type(event); if (type == LIBINPUT_EVENT_DEVICE_ADDED) { - struct libinput_device *device = libinput_event_get_device(event); + device = libinput_event_get_device(event); - struct input_device_data *data = calloc(1, sizeof(*data)); + data = calloc(1, sizeof(*data)); data->flutter_device_id_offset = flutterpi.input.next_unused_flutter_device_id; libinput_device_set_user_data(device, data); @@ -1947,11 +1953,12 @@ static int on_libinput_ready(sd_event_source *s, int fd, uint32_t revents, void .buttons = 0 }; } + } else if (libinput_device_has_capability(device, LIBINPUT_DEVICE_CAP_KEYBOARD)) { + data->keyboard_state = keyboard_state_new(flutterpi.input.keyboard_config, NULL, NULL); } } else if (LIBINPUT_EVENT_IS_TOUCH(type)) { - struct libinput_event_touch *touch_event = libinput_event_get_touch_event(event); - - struct input_device_data *data = libinput_device_get_user_data(libinput_event_get_device(event)); + touch_event = libinput_event_get_touch_event(event); + data = libinput_device_get_user_data(libinput_event_get_device(event)); if ((type == LIBINPUT_EVENT_TOUCH_DOWN) || (type == LIBINPUT_EVENT_TOUCH_MOTION) || (type == LIBINPUT_EVENT_TOUCH_UP)) { int slot = libinput_event_touch_get_slot(touch_event); @@ -2006,8 +2013,8 @@ static int on_libinput_ready(sd_event_source *s, int fd, uint32_t revents, void } } } else if (LIBINPUT_EVENT_IS_POINTER(type)) { - struct libinput_event_pointer *pointer_event = libinput_event_get_pointer_event(event); - struct input_device_data *data = libinput_device_get_user_data(libinput_event_get_device(event)); + pointer_event = libinput_event_get_pointer_event(event); + data = libinput_device_get_user_data(libinput_event_get_device(event)); if (type == LIBINPUT_EVENT_POINTER_MOTION) { double dx = libinput_event_pointer_get_dx(pointer_event); @@ -2139,14 +2146,75 @@ static int on_libinput_ready(sd_event_source *s, int fd, uint32_t revents, void } } else if (LIBINPUT_EVENT_IS_KEYBOARD(type)) { - struct libinput_event_keyboard *keyboard_event = libinput_event_get_keyboard_event(event); + struct keyboard_modifier_state mods; + enum libinput_key_state key_state; + xkb_keysym_t keysym; + uint32_t codepoint, plain_codepoint; + uint16_t evdev_keycode; - uint32_t keycode = libinput_event_keyboard_get_key(keyboard_event); - enum libinput_key_state state = libinput_event_keyboard_get_key_state(keyboard_event); + keyboard_event = libinput_event_get_keyboard_event(event); + data = libinput_device_get_user_data(libinput_event_get_device(event)); + evdev_keycode = libinput_event_keyboard_get_key(keyboard_event); + key_state = libinput_event_keyboard_get_key_state(keyboard_event); + + mods = keyboard_state_get_meta_state(data->keyboard_state); + + ok = keyboard_state_process_key_event( + data->keyboard_state, + evdev_keycode, + (int32_t) key_state, + &keysym, + &codepoint + ); - glfw_key glfw_key = evdev_code_glfw_key[keycode]; - - rawkb_on_keyevent(glfw_key, keycode, state == LIBINPUT_KEY_STATE_PRESSED ? GLFW_PRESS : GLFW_RELEASE); + printf("[key event] keycode: 0x%04X, type: %s, keysym: 0x%08X, codepoint: 0x%08X\n", evdev_keycode, key_state? "down" : " up ", keysym, codepoint); + + plain_codepoint = keyboard_state_get_plain_codepoint(data->keyboard_state, evdev_keycode, 1); + + rawkb_send_gtk_keyevent( + plain_codepoint, + (uint32_t) keysym, + evdev_keycode + 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 + ); + + if (codepoint) { + if (codepoint < 0x80) { + if (isprint(codepoint)) { + textin_on_utf8_char((uint8_t[1]) {codepoint}); + } + } else if (codepoint < 0x800) { + textin_on_utf8_char((uint8_t[2]) { + 0xc0 | (codepoint >> 6), + 0x80 | (codepoint & 0x3f) + }); + } else if (codepoint < 0x10000) { + if (!(codepoint >= 0xD800 && codepoint < 0xE000) && !(codepoint == 0xFFFF)) { + textin_on_utf8_char((uint8_t[3]) { + 0xe0 | (codepoint >> 12), + 0x80 | ((codepoint >> 6) & 0x3f), + 0x80 | (codepoint & 0x3f) + }); + } + } else if (codepoint < 0x110000) { + textin_on_utf8_char((uint8_t[4]) { + 0xf0 | (codepoint >> 18), + 0x80 | ((codepoint >> 12) & 0x3f), + 0x80 | ((codepoint >> 6) & 0x3f), + 0x80 | (codepoint & 0x3f) + }); + } + } + + if (keysym) { + textin_on_xkb_keysym(keysym); + } } libinput_event_destroy(event); @@ -2167,38 +2235,6 @@ static int on_libinput_ready(sd_event_source *s, int fd, uint32_t revents, void return 0; } -static int on_stdin_ready(sd_event_source *s, int fd, uint32_t revents, void *userdata) { - static char buffer[4096]; - glfw_key key; - char *cursor; - char *c; - int ok; - - ok = read(STDIN_FILENO, buffer, sizeof(buffer) - 1); - if (ok == -1) { - perror("[flutter-pi] Could not read from stdin"); - return errno; - } else if (ok == 0) { - fprintf(stderr, "[flutter-pi] WARNING: reached EOF for stdin\n"); - return EBADF; - } - - buffer[ok] = '\0'; - - cursor = buffer; - while (*cursor) { - if (key = console_try_get_key(cursor, &cursor), key != GLFW_KEY_UNKNOWN) { - textin_on_key(key); - } else if (c = console_try_get_utf8char(cursor, &cursor), c != NULL) { - textin_on_utf8_char(c); - } else { - // neither a char nor a (function) key. we don't know when - // we can start parsing the buffer again, so just stop here - break; - } - } -} - static struct libinput *try_create_udev_backed_libinput(void) { #ifdef BUILD_WITHOUT_UDEV_SUPPORT return NULL; @@ -2385,11 +2421,15 @@ static struct libinput *try_create_path_backed_libinput(void) { } static int init_user_input(void) { - sd_event_source *libinput_event_source, *stdin_event_source; + sd_event_source *libinput_event_source; + struct keyboard_config *kbdcfg; struct libinput *libinput; int ok; + libinput_event_source = NULL; + kbdcfg = NULL; libinput = NULL; + if (flutterpi.input.use_paths == false) { libinput = try_create_udev_backed_libinput(); } @@ -2398,7 +2438,6 @@ static int init_user_input(void) { libinput = try_create_path_backed_libinput(); } - libinput_event_source = NULL; if (libinput != NULL) { ok = sd_event_add_io( flutterpi.event_loop, @@ -2423,36 +2462,20 @@ static int init_user_input(void) { # endif return -ok; } + + 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"); + } + } } else { fprintf(stderr, "[flutter-pi] Could not initialize input. Flutter-pi will run without user input.\n"); } - stdin_event_source = NULL; - if (flutterpi.input.disable_text_input == false) { - ok = sd_event_add_io( - flutterpi.event_loop, - &stdin_event_source, - STDIN_FILENO, - EPOLLIN, - on_stdin_ready, - NULL - ); - if (ok < 0) { - fprintf(stderr, "[flutter-pi] Could not add callback for console input. sd_event_add_io: %s\n", strerror(-ok)); - } - - ok = console_make_raw(); - if (ok == 0) { - console_flush_stdin(); - } else { - fprintf(stderr, "[flutter-pi] Could not make stdin raw. Flutter-pi will run without text input support.\n"); - sd_event_source_unrefp(&stdin_event_source); - } - } - flutterpi.input.libinput = libinput; flutterpi.input.libinput_event_source = libinput_event_source; - flutterpi.input.stdin_event_source = stdin_event_source; + flutterpi.input.keyboard_config = kbdcfg; return 0; } diff --git a/src/keyboard.c b/src/keyboard.c new file mode 100644 index 00000000..a7b86234 --- /dev/null +++ b/src/keyboard.c @@ -0,0 +1,369 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + + +static int find_var_offset_in_string(const char *varname, const char *buffer, regmatch_t *match) { + regmatch_t matches[2]; + char *pattern; + int ok; + + ok = asprintf(&pattern, "%s=\"([^\"]*)\"", varname); + if (ok < 0) { + ok = ENOMEM; + goto fail_set_match; + } + + regex_t regex; + ok = regcomp(®ex, pattern, REG_EXTENDED); + if (ok != 0) { + //pregexerr("regcomp", ok, ®ex); + ok = EINVAL; + goto fail_set_match; + } + + ok = regexec(®ex, buffer, 2, matches, 0); + if (ok == REG_NOMATCH) { + ok = EINVAL; + goto fail_free_regex; + } + + if (match != NULL) { + *match = matches[1]; + } + + return 0; + + fail_free_regex: + regfree(®ex); + + fail_set_match: + if (match != NULL) { + match->rm_so = -1; + match->rm_eo = -1; + } + + fail_return_ok: + return ok; +} + +static char *get_value_allocated(const char *varname, const char *buffer) { + regmatch_t match; + char *allocated; + int ok, match_length; + + 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; + } + + match_length = match.rm_eo - match.rm_so; + + allocated = malloc(match_length + 1); + if (allocated == NULL) { + errno = ENOMEM; + return NULL; + } + + strncpy(allocated, buffer + match.rm_so, match_length); + + allocated[match_length] = '\0'; + + return allocated; +} + +static char *load_file(const char *path) { + struct stat s; + int ok, fd; + + ok = open(path, O_RDONLY); + if (ok < 0) { + goto fail_return_null; + } else { + fd = ok; + } + + ok = fstat(fd, &s); + if (ok < 0) { + goto fail_close; + } + + char *buffer = malloc(s.st_size + 1); + if (buffer == NULL) { + errno = ENOMEM; + goto fail_close; + } + + int result = read(fd, buffer, s.st_size); + if (result < 0) { + goto fail_close; + } else if (result == 0) { + errno = EINVAL; + goto fail_close; + } else if (result < s.st_size) { + errno = EINVAL; + goto fail_close; + } + + close(fd); + + buffer[s.st_size] = '\0'; + + return buffer; + + + fail_close: + close(fd); + + fail_return_null: + return NULL; +} + +static struct xkb_keymap *load_default_keymap(struct xkb_context *context) { + char *file = load_file("/etc/default/keyboard"); + if (file == NULL) { + perror("[keyboard] Could not load default keyboard configuration from \"/etc/default/keyboard\". "); + return NULL; + } + + 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); + + free(file); + + struct xkb_rule_names names = { + .rules = NULL, + .model = xkbmodel, + .layout = xkblayout, + .variant = xkbvariant, + .options = xkboptions + }; + + struct xkb_keymap *keymap = xkb_keymap_new_from_names(context, &names, XKB_KEYMAP_COMPILE_NO_FLAGS); + + if (xkbmodel != NULL) free(xkbmodel); + if (xkblayout != NULL) free(xkblayout); + if (xkbvariant != NULL) free(xkbvariant); + if (xkboptions != NULL) free(xkboptions); + + if (keymap == NULL) { + fprintf(stderr, "[keyboard] Could not load default keymap.\n"); + } + + return keymap; +} + +static struct xkb_compose_table *load_default_compose_table(struct xkb_context *context) { + setlocale(LC_ALL, ""); + return xkb_compose_table_new_from_locale(context, setlocale(LC_CTYPE, NULL), XKB_COMPOSE_COMPILE_NO_FLAGS); +} + + +struct keyboard_config *keyboard_config_new(void) { + struct keyboard_config *cfg; + struct xkb_compose_table *compose_table; + struct xkb_context *ctx; + struct xkb_keymap *keymap; + struct xkb_state *plain_state; + int ok; + + cfg = malloc(sizeof *cfg); + if (cfg == NULL) { + errno = ENOMEM; + goto fail_return_null; + } + + ctx = xkb_context_new(XKB_CONTEXT_NO_FLAGS); + if (ctx == NULL) { + goto fail_free_cfg; + } + + compose_table = load_default_compose_table(ctx); + if (compose_table == NULL) { + goto fail_free_context; + } + + keymap = load_default_keymap(ctx); + if (keymap == NULL) { + goto fail_free_compose_table; + } + + cfg->context = ctx; + cfg->default_compose_table = compose_table; + cfg->default_keymap = keymap; + + return cfg; + + fail_free_keymap: + xkb_keymap_unref(keymap); + + fail_free_compose_table: + xkb_compose_table_unref(compose_table); + + fail_free_context: + xkb_context_unref(ctx); + + fail_free_cfg: + free(cfg); + + fail_return_null: + return NULL; +} + +void keyboard_config_destroy(struct keyboard_config *config) { + xkb_keymap_unref(config->default_keymap); + xkb_compose_table_unref(config->default_compose_table); + xkb_context_unref(config->context); + free(config); +} + + +struct keyboard_state *keyboard_state_new( + struct keyboard_config *config, + struct xkb_keymap *keymap_override, + struct xkb_compose_table *compose_table_override +) { + struct keyboard_state *state; + struct xkb_compose_state *compose_state; + struct xkb_state *xkb_state, *plain_xkb_state; + + state = malloc(sizeof *state); + if (state == NULL) { + errno = ENOMEM; + goto fail_return_null; + } + + xkb_state = xkb_state_new(keymap_override != NULL ? keymap_override : config->default_keymap); + if (xkb_state == NULL) { + goto fail_free_state; + } + + plain_xkb_state = xkb_state_new(keymap_override != NULL ? keymap_override : config->default_keymap); + if (plain_xkb_state == NULL) { + 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) { + goto fail_free_xkb_state; + } + + state->config = config; + state->state = xkb_state; + state->plain_state = plain_xkb_state; + state->compose_state = compose_state; + + return state; + + fail_free_plain_xkb_state: + xkb_state_unref(plain_xkb_state); + + fail_free_xkb_state: + xkb_state_unref(xkb_state); + + fail_free_state: + free(state); + + fail_return_null: + return NULL; +} + +void keyboard_state_destroy( + struct keyboard_state *state +) { + xkb_compose_state_unref(state->compose_state); + xkb_state_unref(state->plain_state); + xkb_state_unref(state->state); + free(state); +} + +int keyboard_state_process_key_event( + struct keyboard_state *state, + uint16_t evdev_keycode, + int32_t evdev_value, + xkb_keysym_t *keysym_out, + uint32_t *codepoint_out +) { + enum xkb_compose_feed_result feed_result; + enum xkb_compose_status compose_status; + xkb_keycode_t xkb_keycode; + xkb_keysym_t keysym; + uint32_t codepoint; + + /** + * evdev_value = 0: release + * evdev_value = 1: press + * evdev_value = 2: repeat + */ + + keysym = 0; + codepoint = 0; + xkb_keycode = evdev_keycode + 8; + + if (evdev_value) { + keysym = xkb_state_key_get_one_sym(state->state, xkb_keycode); + + feed_result = xkb_compose_state_feed(state->compose_state, keysym); + compose_status = xkb_compose_state_get_status(state->compose_state); + if (feed_result == XKB_COMPOSE_FEED_ACCEPTED && compose_status == XKB_COMPOSE_COMPOSING) { + keysym = XKB_KEY_NoSymbol; + } + + if (compose_status == XKB_COMPOSE_COMPOSED) { + keysym = xkb_compose_state_get_one_sym(state->compose_state); + xkb_compose_state_reset(state->compose_state); + } else if (compose_status == XKB_COMPOSE_CANCELLED) { + xkb_compose_state_reset(state->compose_state); + } + + codepoint = xkb_keysym_to_utf32(keysym); + } + + xkb_state_update_key(state->state, xkb_keycode, (enum xkb_key_direction) evdev_value); + + if (keysym_out) *keysym_out = keysym; + if (codepoint_out) *codepoint_out = codepoint; + + return 0; +} + +uint32_t keyboard_state_get_plain_codepoint( + struct keyboard_state *state, + uint16_t evdev_keycode, + int32_t evdev_value +) { + xkb_keycode_t xkb_keycode = evdev_keycode + 8; + + if (evdev_value) { + return xkb_state_key_get_utf32(state->plain_state, xkb_keycode); + } + + return 0; +} \ No newline at end of file diff --git a/src/plugins/raw_keyboard.c b/src/plugins/raw_keyboard.c index 3649641a..9d158d37 100644 --- a/src/plugins/raw_keyboard.c +++ b/src/plugins/raw_keyboard.c @@ -7,36 +7,80 @@ #include #include #include +#include #include -static struct { - // same as mods, just that it differentiates between left and right-sided modifiers. - uint16_t leftright_mods; - glfw_keymod_map mods; - bool initialized; -} raw_keyboard = {0}; - -static int send_glfw_keyevent(uint32_t code_point, glfw_key key_code, uint32_t scan_code, glfw_keymod_map mods, bool is_down) { +int rawkb_send_android_keyevent( + uint32_t flags, + uint32_t code_point, + unsigned int key_code, + uint32_t plain_code_point, + uint32_t scan_code, + uint32_t meta_state, + uint32_t source, + uint16_t vendor_id, + uint16_t product_id, + uint16_t device_id, + int repeat_count, + bool is_down, + char *character +) { + /** + * keymap: android + * flags: flags + * codePoint: code_point + * keyCode: key_code + * plainCodePoint: plain_code_point + * scanCode: scan_code + * metaState: meta_state + * source: source + * vendorId: vendor_id + * productId: product_id + * deviceId: device_id + * repeatCount: repeatCount, + * type: is_down? "keydown" : "keyup" + * character: character + */ + return platch_send( KEY_EVENT_CHANNEL, &(struct platch_obj) { .codec = kJSONMessageCodec, .json_value = { .type = kJsonObject, - .size = 7, - .keys = (char*[7]) { - "keymap", "toolkit", "unicodeScalarValues", "keyCode", "scanCode", - "modifiers", "type" + .size = 14, + .keys = (char*[14]) { + "keymap", + "flags", + "codePoint", + "keyCode", + "plainCodePoint", + "scanCode", + "metaState", + "source", + "vendorId", + "productId", + "deviceId", + "repeatCount", + "type", + "character" }, - .values = (struct json_value[7]) { - {.type = kJsonString, .string_value = "linux"}, - {.type = kJsonString, .string_value = "glfw"}, - {.type = kJsonNumber, .number_value = code_point}, - {.type = kJsonNumber, .number_value = key_code}, - {.type = kJsonNumber, .number_value = scan_code}, - {.type = kJsonNumber, .number_value = mods}, - {.type = kJsonString, .string_value = is_down? "keydown" : "keyup"} + .values = (struct json_value[14]) { + /* keymap */ {.type = kJsonString, .string_value = "android"}, + /* flags */ {.type = kJsonNumber, .number_value = flags}, + /* codePoint */ {.type = kJsonNumber, .number_value = code_point}, + /* keyCode */ {.type = kJsonNumber, .number_value = key_code}, + /* plainCodePoint */ {.type = kJsonNumber, .number_value = code_point}, + /* scanCode */ {.type = kJsonNumber, .number_value = scan_code}, + /* metaState */ {.type = kJsonNumber, .number_value = meta_state}, + /* source */ {.type = kJsonNumber, .number_value = source}, + /* vendorId */ {.type = kJsonNumber, .number_value = vendor_id}, + /* productId */ {.type = kJsonNumber, .number_value = product_id}, + /* deviceId */ {.type = kJsonNumber, .number_value = device_id}, + /* repeatCount */ {.type = kJsonNumber, .number_value = repeat_count}, + /* type */ {.type = kJsonString, .string_value = is_down? "keydown" : "keyup"}, + /* character */ {.type = character? kJsonString : kJsonNull, .string_value = character} } } }, @@ -46,88 +90,60 @@ static int send_glfw_keyevent(uint32_t code_point, glfw_key key_code, uint32_t s ); } -int rawkb_on_keyevent(glfw_key key, uint32_t scan_code, glfw_key_action action) { - glfw_keymod_map mods_after = raw_keyboard.mods; - uint16_t lrmods_after = raw_keyboard.leftright_mods; - glfw_keymod mod; - bool send; - - if (!raw_keyboard.initialized) return 0; - - // flutter's glfw key adapter does not distinguish between left- and right-sided modifier keys. - // so we implicitly combine the state of left and right-sided keys - mod = GLFW_KEYMOD_FOR_KEY(key); - send = !mod; - - if (mod && ((action == GLFW_PRESS) || (action == GLFW_RELEASE))) { - lrmods_after = raw_keyboard.leftright_mods; - - switch (mod) { - case GLFW_MOD_SHIFT: - case GLFW_MOD_CONTROL: - case GLFW_MOD_ALT: - case GLFW_MOD_SUPER: ; - uint16_t sided_mod = mod; - - if (GLFW_KEY_IS_RIGHTSIDED(key)) - sided_mod = sided_mod << 8; +int rawkb_send_gtk_keyevent( + uint32_t unicode_scalar_values, + uint32_t key_code, + uint32_t scan_code, + uint32_t modifiers, + bool is_down +) { + /** + * keymap: linux + * toolkit: glfw + * unicodeScalarValues: code_point + * keyCode: key_code + * scanCode: scan_code + * modifiers: mods + * type: is_down? "keydown" : "keyup" + */ - if (action == GLFW_PRESS) { - lrmods_after |= sided_mod; - } else if (action == GLFW_RELEASE) { - lrmods_after &= ~sided_mod; + return platch_send( + KEY_EVENT_CHANNEL, + &(struct platch_obj) { + .codec = kJSONMessageCodec, + .json_value = { + .type = kJsonObject, + .size = 7, + .keys = (char*[7]) { + "keymap", + "toolkit", + "unicodeScalarValues", + "keyCode", + "scanCode", + "modifiers", + "type" + }, + .values = (struct json_value[7]) { + /* keymap */ {.type = kJsonString, .string_value = "linux"}, + /* toolkit */ {.type = kJsonString, .string_value = "gtk"}, + /* unicodeScalarValues */ {.type = kJsonNumber, .number_value = unicode_scalar_values}, + /* keyCode */ {.type = kJsonNumber, .number_value = key_code}, + /* scanCode */ {.type = kJsonNumber, .number_value = scan_code}, + /* modifiers */ {.type = kJsonNumber, .number_value = modifiers}, + /* type */ {.type = kJsonString, .string_value = is_down? "keydown" : "keyup"} } - break; - case GLFW_MOD_CAPS_LOCK: - case GLFW_MOD_NUM_LOCK: - if (action == GLFW_PRESS) - lrmods_after ^= mod; - break; - default: - break; - } - - mods_after = lrmods_after | (lrmods_after >> 8); - if (mods_after != raw_keyboard.mods) - send = true; - } - - switch (key) { - case GLFW_KEY_RIGHT_SHIFT: - key = GLFW_KEY_LEFT_SHIFT; - break; - case GLFW_KEY_RIGHT_CONTROL: - key = GLFW_KEY_LEFT_CONTROL; - break; - case GLFW_KEY_RIGHT_ALT: - key = GLFW_KEY_LEFT_ALT; - break; - case GLFW_KEY_RIGHT_SUPER: - key = GLFW_KEY_LEFT_SUPER; - break; - default: break; - } - - if (send) { - send_glfw_keyevent(0, key, scan_code, raw_keyboard.mods, action != GLFW_RELEASE); - } - - raw_keyboard.leftright_mods = lrmods_after; - raw_keyboard.mods = mods_after; - - return 0; + } + }, + kJSONMessageCodec, + NULL, + NULL + ); } int rawkb_init(void) { - raw_keyboard.leftright_mods = 0; - raw_keyboard.mods = 0; - raw_keyboard.initialized = true; - return 0; } int rawkb_deinit(void) { - raw_keyboard.initialized = false; - return 0; } \ No newline at end of file diff --git a/src/plugins/text_input.c b/src/plugins/text_input.c index 755f610c..0a855f34 100644 --- a/src/plugins/text_input.c +++ b/src/plugins/text_input.c @@ -6,11 +6,16 @@ #include #include +#include #include struct { - int32_t transaction_id; + int64_t connection_id; enum text_input_type input_type; + bool allow_signs; + bool has_allow_signs; + bool allow_decimal; + bool has_allow_decimal; bool autocorrect; enum text_input_action input_action; char text[TEXT_INPUT_MAX_CHARS]; @@ -20,351 +25,581 @@ struct { int composing_base, composing_extent; bool warned_about_autocorrect; } text_input = { - .transaction_id = -1 + .connection_id = -1 }; -int textin_on_receive(char *channel, struct platch_obj *object, FlutterPlatformMessageResponseHandle *responsehandle) { - struct json_value jsvalue, *temp, *temp2, *state, *config; - int ok; +/** + * UTF8 utility functions + */ +static inline uint8_t utf8_symbol_length(uint8_t c) { + if (!(c & 0b10000000)) { + return 1; + } else if (!(c & 0b01000000)) { + // we are in a follow byte + return 0; + } else if (c & 0b00100000) { + return 2; + } else if (c & 0b00010000) { + return 3; + } else if (c & 0b00001000) { + return 4; + } - if STREQ("TextInput.setClient", object->method) { - /* - * TextInput.setClient(List) - * Establishes a new transaction. The argument is - * a [List] whose first value is an integer representing a previously - * unused transaction identifier, and the second is a [String] with a - * JSON-encoded object with five keys, as obtained from - * [TextInputConfiguration.toJSON]. This method must be invoked before any - * others (except `TextInput.hide`). See [TextInput.attach]. - */ - - if ((object->json_arg.type != kJsonArray) || (object->json_arg.size != 2)) { - return platch_respond_illegal_arg_json( - responsehandle, - "Expected `arg` to be an array with length 2." - ); - } + return 0; +} - if (object->json_arg.array[0].type != kJsonNumber) { - return platch_respond_illegal_arg_json( - responsehandle, - "Expected transaction id to be a number." - ); - } +static inline uint8_t *symbol_at(unsigned int symbol_index) { + uint8_t *cursor = text_input.text; - if (object->json_arg.array[1].type != kJsonObject) { - return platch_respond_illegal_arg_json( - responsehandle, - "Expected text input configuration to be a String" - ); - } + for (; symbol_index && *cursor; symbol_index--) + cursor += utf8_symbol_length(*cursor); - struct json_value *config = &object->json_arg.array[1]; + return symbol_index? NULL : cursor; +} - if (config->type != kJsonObject) { - return platch_respond_illegal_arg_json( - responsehandle, - "Expected decoded text input configuration to be an Object" - ); - } +static inline int to_byte_index(unsigned int symbol_index) { + char *cursor = text_input.text; - enum text_input_type input_type; - bool autocorrect; - enum text_input_action input_action; + while ((*cursor) && (symbol_index--)) + cursor += utf8_symbol_length(*cursor); - // AUTOCORRECT - temp = jsobject_get(config, "autocorrect"); - if (!(temp && ((temp->type == kJsonTrue) || (temp->type == kJsonFalse)))) - goto invalid_config; + if (*cursor) + return cursor - text_input.text; - autocorrect = temp->type == kJsonTrue; - - // INPUT ACTION - temp = jsobject_get(config, "inputAction"); - if (!(temp && (temp->type == kJsonString))) - goto invalid_config; - - if STREQ("TextInputAction.none", temp->string_value) - input_action = kTextInputActionNone; - else if STREQ("TextInputAction.unspecified", temp->string_value) - input_action = kTextInputActionUnspecified; - else if STREQ("TextInputAction.done", temp->string_value) - input_action = kTextInputActionDone; - else if STREQ("TextInputAction.go", temp->string_value) - input_action = kTextInputActionGo; - else if STREQ("TextInputAction.search", temp->string_value) - input_action = kTextInputActionSearch; - else if STREQ("TextInputAction.send", temp->string_value) - input_action = kTextInputActionSend; - else if STREQ("TextInputAction.next", temp->string_value) - input_action = kTextInputActionNext; - else if STREQ("TextInputAction.previous", temp->string_value) - input_action = kTextInputActionPrevious; - else if STREQ("TextInputAction.continueAction", temp->string_value) - input_action = kTextInputActionContinueAction; - else if STREQ("TextInputAction.join", temp->string_value) - input_action = kTextInputActionJoin; - else if STREQ("TextInputAction.route", temp->string_value) - input_action = kTextInputActionRoute; - else if STREQ("TextInputAction.emergencyCall", temp->string_value) - input_action = kTextInputActionEmergencyCall; - else if STREQ("TextInputAction.newline", temp->string_value) - input_action = kTextInputActionNewline; - else - goto invalid_config; - - - // INPUT TYPE - temp = jsobject_get(config, "inputType"); - - if (!temp || temp->type != kJsonObject) - goto invalid_config; - - - temp2 = jsobject_get(temp, "name"); - - if (!temp2 || temp2->type != kJsonString) - goto invalid_config; - - if STREQ("TextInputType.text", temp2->string_value) { - input_type = kInputTypeText; - } else if STREQ("TextINputType.multiline", temp2->string_value) { - input_type = kInputTypeMultiline; - } else if STREQ("TextInputType.number", temp2->string_value) { - input_type = kInputTypeNumber; - } else if STREQ("TextInputType.phone", temp2->string_value) { - input_type = kInputTypePhone; - } else if STREQ("TextInputType.datetime", temp2->string_value) { - input_type = kInputTypeDatetime; - } else if STREQ("TextInputType.emailAddress", temp2->string_value) { - input_type = kInputTypeEmailAddress; - } else if STREQ("TextInputType.url", temp2->string_value) { - input_type = kInputTypeUrl; - } else if STREQ("TextInputType.visiblePassword", temp2->string_value) { - input_type = kInputTypeVisiblePassword; - } else { - goto invalid_config; - } + return -1; +} + +static inline int to_symbol_index(unsigned int byte_index) { + char *cursor = text_input.text; + char *target_cursor = cursor + byte_index; + int symbol_index = 0; - // TRANSACTION ID - int32_t new_id = (int32_t) object->json_arg.array[0].number_value; + while ((*cursor) && (cursor < target_cursor)) { + cursor += utf8_symbol_length(*cursor); + symbol_index++; + } - // everything okay, apply the new text editing config - text_input.transaction_id = new_id; - text_input.autocorrect = autocorrect; - text_input.input_action = input_action; - text_input.input_type = input_type; + return cursor < target_cursor? -1 : symbol_index; +} - if (autocorrect && (!text_input.warned_about_autocorrect)) { - printf("[text_input] warning: flutter requested native autocorrect, which" - "is not supported by flutter-pi.\n"); - text_input.warned_about_autocorrect = true; - } +/** + * Platform message callbacks + */ +static int on_set_client( + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + enum text_input_action input_action; + enum text_input_type input_type; + struct json_value jsvalue, *temp, *temp2, *state, *config; + int64_t transaction_id; + bool autocorrect, allow_signs, allow_decimal, has_allow_signs, has_allow_decimal; + int ok; - return platch_respond( + /* + * TextInput.setClient(List) + * Establishes a new transaction. The argument is + * a [List] whose first value is an integer representing a previously + * unused transaction identifier, and the second is a [String] with a + * JSON-encoded object with five keys, as obtained from + * [TextInputConfiguration.toJSON]. This method must be invoked before any + * others (except `TextInput.hide`). See [TextInput.attach]. + */ + + if ((object->json_arg.type != kJsonArray) || (object->json_arg.size != 2)) { + return platch_respond_illegal_arg_json( responsehandle, - &(struct platch_obj) { - .codec = kJSONMethodCallResponse, - .success = true, - .json_result = {.type = kJsonNull} - } + "Expected `arg` to be an array with length 2." ); + } - // invalid config given to setClient - invalid_config: - return platch_respond_illegal_arg_json( - responsehandle, - "Expected decoded text input configuration to at least contain values for \"autocorrect\"" - " and \"inputAction\"" - ); + if (object->json_arg.array[0].type != kJsonNumber) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg[0]` to be a number" + ); + } - } else if STREQ("TextInput.show", object->method) { - /* - * TextInput.show() - * Show the keyboard. See [TextInputConnection.show]. - * - */ - - // do nothing since we use a physical keyboard. - return platch_respond( + if (object->json_arg.array[1].type != kJsonObject) { + return platch_respond_illegal_arg_json( responsehandle, - &(struct platch_obj) { - .codec = kJSONMethodCallResponse, - .success = true, - .json_result = {.type = kJsonNull} - } + "Expected `arg[1]` to be an map." + ); + } + + config = &object->json_arg.array[1]; + + // AUTOCORRECT + temp = jsobject_get(config, "autocorrect"); + if (temp == NULL || (temp->type != kJsonTrue && temp->type != kJsonFalse)) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg[1]['autocorrect']` to be a boolean." + ); + } else { + autocorrect = temp->type == kJsonTrue; + } + + // INPUT ACTION + temp = jsobject_get(config, "inputAction"); + if (temp == NULL || temp->type != kJsonString) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg[1]['inputAction']` to be a string-ification of `TextInputAction`." + ); + } + + if STREQ("TextInputAction.none", temp->string_value) + input_action = kTextInputActionNone; + else if STREQ("TextInputAction.unspecified", temp->string_value) + input_action = kTextInputActionUnspecified; + else if STREQ("TextInputAction.done", temp->string_value) + input_action = kTextInputActionDone; + else if STREQ("TextInputAction.go", temp->string_value) + input_action = kTextInputActionGo; + else if STREQ("TextInputAction.search", temp->string_value) + input_action = kTextInputActionSearch; + else if STREQ("TextInputAction.send", temp->string_value) + input_action = kTextInputActionSend; + else if STREQ("TextInputAction.next", temp->string_value) + input_action = kTextInputActionNext; + else if STREQ("TextInputAction.previous", temp->string_value) + input_action = kTextInputActionPrevious; + else if STREQ("TextInputAction.continueAction", temp->string_value) + input_action = kTextInputActionContinueAction; + else if STREQ("TextInputAction.join", temp->string_value) + input_action = kTextInputActionJoin; + else if STREQ("TextInputAction.route", temp->string_value) + input_action = kTextInputActionRoute; + else if STREQ("TextInputAction.emergencyCall", temp->string_value) + input_action = kTextInputActionEmergencyCall; + else if STREQ("TextInputAction.newline", temp->string_value) + input_action = kTextInputActionNewline; + else + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg[1]['inputAction']` to be a string-ification of `TextInputAction`." ); - } else if STREQ("TextInput.setEditingState", object->method) { - /* - * TextInput.setEditingState(Map textEditingValue) - * Update the value in the text editing control. The argument is a - * [String] with a JSON-encoded object with seven keys, as - * obtained from [TextEditingValue.toJSON]. - * See [TextInputConnection.setEditingState]. - * - */ - - state = &object->json_arg; - - if (state->type != kJsonObject) { - return platch_respond_illegal_arg_json( - responsehandle, - "Expected decoded text editing value to be an Object" - ); - } - char *text; - int selection_base, selection_extent, composing_base, composing_extent; - bool selection_affinity_is_downstream, selection_is_directional; + // INPUT TYPE + temp = jsobject_get(config, "inputType"); + if (temp == NULL || temp->type != kJsonObject) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg[1]['inputType']` to be a map." + ); + } - temp = jsobject_get(state, "text"); - if (temp && (temp->type == kJsonString)) text = temp->string_value; - else goto invalid_editing_value; + temp2 = jsobject_get(temp, "signed"); + if (temp2 == NULL || temp2->type == kJsonNull) { + has_allow_signs = false; + } else if (temp2->type == kJsonTrue || temp2->type == kJsonFalse) { + has_allow_signs = true; + allow_signs = temp2->type == kJsonTrue; + } else { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg[1]['inputType']['signed']` to be a boolean or null." + ); + } - temp = jsobject_get(state, "selectionBase"); - if (temp && (temp->type == kJsonNumber)) selection_base = (int) temp->number_value; - else goto invalid_editing_value; + temp2 = jsobject_get(temp, "decimal"); + if (temp2 == NULL || temp2->type == kJsonNull) { + has_allow_decimal = false; + } else if (temp2->type == kJsonTrue || temp2->type == kJsonFalse) { + has_allow_decimal = true; + allow_signs = temp2->type == kJsonTrue; + } else { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg[1]['inputType']['decimal']` to be a boolean or null." + ); + } - temp = jsobject_get(state, "selectionExtent"); - if (temp && (temp->type == kJsonNumber)) selection_extent = (int) temp->number_value; - else goto invalid_editing_value; + temp2 = jsobject_get(temp, "name"); + if (temp2 == NULL || temp2->type != kJsonString) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg[1]['inputType']['name']` to be a string-ification of `TextInputType`." + ); + } - temp = jsobject_get(state, "selectionAffinity"); - if (temp && (temp->type == kJsonString)) { - if STREQ("TextAffinity.downstream", temp->string_value) { - selection_affinity_is_downstream = true; - } else if STREQ("TextAffinity.upstream", temp->string_value) { - selection_affinity_is_downstream = false; - } else { - goto invalid_editing_value; - } - } else { - goto invalid_editing_value; + if STREQ("TextInputType.text", temp2->string_value) { + input_type = kInputTypeText; + } else if STREQ("TextInputType.multiline", temp2->string_value) { + input_type = kInputTypeMultiline; + } else if STREQ("TextInputType.number", temp2->string_value) { + input_type = kInputTypeNumber; + } else if STREQ("TextInputType.phone", temp2->string_value) { + input_type = kInputTypePhone; + } else if STREQ("TextInputType.datetime", temp2->string_value) { + input_type = kInputTypeDatetime; + } else if STREQ("TextInputType.emailAddress", temp2->string_value) { + input_type = kInputTypeEmailAddress; + } else if STREQ("TextInputType.url", temp2->string_value) { + input_type = kInputTypeUrl; + } else if STREQ("TextInputType.visiblePassword", temp2->string_value) { + input_type = kInputTypeVisiblePassword; + } else if STREQ("TextInputType.name", temp2->string_value) { + input_type = kInputTypeName; + } else if STREQ("TextInputType.address", temp2->string_value) { + input_type = kInputTypeAddress; + } else { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg[1]['inputType']['name']` to be a string-ification of `TextInputType`." + ); + } + + // TRANSACTION ID + int32_t new_id = (int32_t) object->json_arg.array[0].number_value; + + // everything okay, apply the new text editing config + text_input.connection_id = new_id; + text_input.autocorrect = autocorrect; + text_input.input_action = input_action; + text_input.input_type = input_type; + + if (autocorrect && !text_input.warned_about_autocorrect) { + printf("[text_input] warning: flutter requested native autocorrect, which" + "is not supported by flutter-pi.\n"); + text_input.warned_about_autocorrect = true; + } + + return platch_respond( + responsehandle, + &(struct platch_obj) { + .codec = kJSONMethodCallResponse, + .success = true, + .json_result = {.type = kJsonNull} } + ); +} - temp = jsobject_get(state, "selectionIsDirectional"); - if (temp && (temp->type == kJsonTrue || temp->type == kJsonFalse)) { - selection_is_directional = temp->type == kJsonTrue; - } else { - goto invalid_editing_value; +static int on_hide( + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + /* + * TextInput.hide() + * Hide the keyboard. Unlike the other methods, this can be called + * at any time. See [TextInputConnection.close]. + * + */ + + // do nothing since we use a physical keyboard. + return platch_respond( + responsehandle, + &(struct platch_obj) { + .codec = kJSONMethodCallResponse, + .success = true, + .json_result = {.type = kJsonNull} } + ); +} - temp = jsobject_get(state, "composingBase"); - if (temp && (temp->type == kJsonNumber)) composing_base = (int) temp->number_value; - else goto invalid_editing_value; +static int on_clear_client( + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + /* + * TextInput.clearClient() + * End the current transaction. The next method called must be + * `TextInput.setClient` (or `TextInput.hide`). + * See [TextInputConnection.close]. + * + */ + + text_input.connection_id = -1; + + return platch_respond( + responsehandle, + &(struct platch_obj) { + .codec = kJSONMethodCallResponse, + .success = true, + .json_result = {.type = kJsonNull} + } + ); +} - temp = jsobject_get(state, "composingExtent"); - if (temp && (temp->type == kJsonNumber)) composing_extent = (int) temp->number_value; - else goto invalid_editing_value; +static int on_set_editing_state( + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + struct json_value jsvalue, *temp, *temp2, *state, *config; + char *text; + bool selection_affinity_is_downstream, selection_is_directional; + int selection_base, selection_extent, composing_base, composing_extent; + int ok; + /* + * TextInput.setEditingState(Map textEditingValue) + * Update the value in the text editing control. The argument is a + * [String] with a JSON-encoded object with seven keys, as + * obtained from [TextEditingValue.toJSON]. + * See [TextInputConnection.setEditingState]. + * + */ - snprintf(text_input.text, sizeof(text_input.text), "%s", text); - text_input.selection_base = selection_base; - text_input.selection_extent = selection_extent; - text_input.selection_affinity_is_downstream = selection_affinity_is_downstream; - text_input.selection_is_directional = selection_is_directional; - text_input.composing_base = composing_base; - text_input.composing_extent = composing_extent; + state = &object->json_arg; - return platch_respond( + if (state->type != kJsonObject) { + return platch_respond_illegal_arg_json( responsehandle, - &(struct platch_obj) { - .codec = kJSONMethodCallResponse, - .success = true, - .json_result = {.type = kJsonNull} - } + "Expected `arg` to be a map." ); + } + + temp = jsobject_get(state, "text"); + if (temp == NULL || temp->type != kJsonString) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg['text']` to be a string." + ); + } else { + text = temp->string_value; + } - invalid_editing_value: + temp = jsobject_get(state, "selectionBase"); + if (temp == NULL || temp->type != kJsonNumber) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg['selectionBase']` to be a number." + ); + } else { + selection_base = (int) temp->number_value; + } + + temp = jsobject_get(state, "selectionExtent"); + if (temp == NULL || temp->type != kJsonNumber) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg['selectionExtent']` to be a number." + ); + } else { + selection_extent = (int) temp->number_value; + } + + temp = jsobject_get(state, "selectionAffinity"); + if (temp == NULL || temp->type != kJsonString) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg['selectionAffinity']` to be a string-ification of `TextAffinity`." + ); + } else { + if STREQ("TextAffinity.downstream", temp->string_value) { + selection_affinity_is_downstream = true; + } else if STREQ("TextAffinity.upstream", temp->string_value) { + selection_affinity_is_downstream = false; + } else { return platch_respond_illegal_arg_json( responsehandle, - "Expected decoded text editing value to be a valid" - " JSON representation of a text editing value" + "Expected `arg['selectionAffinity']` to be a string-ification of `TextAffinity`." ); + } + } - } else if STREQ("TextInput.clearClient", object->method) { - /* - * TextInput.clearClient() - * End the current transaction. The next method called must be - * `TextInput.setClient` (or `TextInput.hide`). - * See [TextInputConnection.close]. - * - */ - - text_input.transaction_id = -1; + temp = jsobject_get(state, "selectionIsDirectional"); + if (temp == NULL || (temp->type != kJsonTrue && temp->type != kJsonFalse)) { + return platch_respond_illegal_arg_json( + responsehandle, + "Expected `arg['selectionIsDirectional']` to be a bool." + ); + } else { + selection_is_directional = temp->type == kJsonTrue; + } - return platch_respond( + temp = jsobject_get(state, "composingBase"); + if (temp == NULL || temp->type != kJsonNumber) { + return platch_respond_illegal_arg_json( responsehandle, - &(struct platch_obj) { - .codec = kJSONMethodCallResponse, - .success = true, - .json_result = {.type = kJsonNull} - } + "Expected `arg['composingBase']` to be a number." ); - } else if STREQ("TextInput.hide", object->method) { - /* - * TextInput.hide() - * Hide the keyboard. Unlike the other methods, this can be called - * at any time. See [TextInputConnection.close]. - * - */ - - // do nothing since we use a physical keyboard. - return platch_respond( + } else { + composing_base = (int) temp->number_value; + } + + temp = jsobject_get(state, "composingExtent"); + if (temp == NULL || temp->type != kJsonNumber) { + return platch_respond_illegal_arg_json( responsehandle, - &(struct platch_obj) { - .codec = kJSONMethodCallResponse, - .success = true, - .json_result = {.type = kJsonNull} - } + "Expected `arg['composingExtent']` to be a number." ); + } else { + composing_extent = (int) temp->number_value; } - return platch_respond_not_implemented(responsehandle); + strncpy(text_input.text, text, TEXT_INPUT_MAX_CHARS); + text_input.selection_base = selection_base; + text_input.selection_extent = selection_extent; + text_input.selection_affinity_is_downstream = selection_affinity_is_downstream; + text_input.selection_is_directional = selection_is_directional; + text_input.composing_base = composing_base; + text_input.composing_extent = composing_extent; + + return platch_respond( + responsehandle, + &(struct platch_obj) { + .codec = kJSONMethodCallResponse, + .success = true, + .json_result = {.type = kJsonNull} + } + ); } +static int on_show( + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + /* + * TextInput.show() + * Show the keyboard. See [TextInputConnection.show]. + * + */ + + // do nothing since we use a physical keyboard. + return platch_respond( + responsehandle, + &(struct platch_obj) { + .codec = kJSONMethodCallResponse, + .success = true, + .json_result = {.type = kJsonNull} + } + ); +} -int textin_sync_editing_state() { - return platch_send( - TEXT_INPUT_CHANNEL, +static int on_request_autofill( + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + return platch_respond( + responsehandle, + &(struct platch_obj) { + .codec = kJSONMethodCallResponse, + .success = true, + .json_result = {.type = kJsonNull} + } + ); +} + +static int on_set_editable_size_and_transform( + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + return platch_respond( + responsehandle, + &(struct platch_obj) { + .codec = kJSONMethodCallResponse, + .success = true, + .json_result = {.type = kJsonNull} + } + ); +} + +static int on_set_style( + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + return platch_respond( + responsehandle, + &(struct platch_obj) { + .codec = kJSONMethodCallResponse, + .success = true, + .json_result = {.type = kJsonNull} + } + ); +} + +static int on_finish_autofill_context( + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + return platch_respond( + responsehandle, &(struct platch_obj) { - .codec = kJSONMethodCall, - .method = "TextInputClient.updateEditingState", - .json_arg = { - .type = kJsonArray, - .size = 2, - .array = (struct json_value[2]) { - {.type = kJsonNumber, .number_value = text_input.transaction_id}, - {.type = kJsonObject, .size = 7, - .keys = (char*[7]) { - "text", "selectionBase", "selectionExtent", "selectionAffinity", - "selectionIsDirectional", "composingBase", "composingExtent" + .codec = kJSONMethodCallResponse, + .success = true, + .json_result = {.type = kJsonNull} + } + ); +} + +static int on_receive( + char *channel, + struct platch_obj *object, + FlutterPlatformMessageResponseHandle *responsehandle +) { + if STREQ("TextInput.setClient", object->method) { + return on_set_client(object, responsehandle); + } else if STREQ("TextInput.hide", object->method) { + return on_hide(object, responsehandle); + } else if STREQ("TextInput.clearClient", object->method) { + return on_clear_client(object, responsehandle); + } else if STREQ("TextInput.setEditingState", object->method) { + return on_set_editing_state(object, responsehandle); + } else if STREQ("TextInput.show", object->method) { + return on_show(object, responsehandle); + } else if STREQ("TextInput.requestAutofill", object->method) { + return on_request_autofill(object, responsehandle); + } else if STREQ("TextInput.setEditableSizeAndTransform", object->method) { + return on_set_style(object, responsehandle); + } else if STREQ("TextInput.setStyle", object->method) { + return on_set_style(object, responsehandle); + } else if STREQ("TextInput.finishAutofillContext", object->method) { + return on_finish_autofill_context(object, responsehandle); + } + + return platch_respond_not_implemented(responsehandle); +} + +static int client_update_editing_state( + double connection_id, + char *text, + double selection_base, + double selection_extent, + bool selection_affinity_is_downstream, + bool selection_is_directional, + double composing_base, + double composing_extent +) { + return platch_call_json( + TEXT_INPUT_CHANNEL, + "TextInputClient.updateEditingState", + &(struct json_value) { + .type = kJsonArray, + .size = 2, + .array = (struct json_value[2]) { + {.type = kJsonNumber, .number_value = connection_id}, + {.type = kJsonObject, .size = 7, + .keys = (char*[7]) { + "text", "selectionBase", "selectionExtent", "selectionAffinity", + "selectionIsDirectional", "composingBase", "composingExtent" + }, + .values = (struct json_value[7]) { + {.type = kJsonString, .string_value = text}, + {.type = kJsonNumber, .number_value = selection_base}, + {.type = kJsonNumber, .number_value = selection_extent}, + { + .type = kJsonString, + .string_value = selection_affinity_is_downstream ? + "TextAffinity.downstream" : "TextAffinity.upstream" }, - .values = (struct json_value[7]) { - {.type = kJsonString, .string_value = text_input.text}, - {.type = kJsonNumber, .number_value = text_input.selection_base}, - {.type = kJsonNumber, .number_value = text_input.selection_extent}, - { - .type = kJsonString, - .string_value = text_input.selection_affinity_is_downstream ? - "TextAffinity.downstream" : "TextAffinity.upstream" - }, - {.type = text_input.selection_is_directional? kJsonTrue : kJsonFalse}, - {.type = kJsonNumber, .number_value = text_input.composing_base}, - {.type = kJsonNumber, .number_value = text_input.composing_extent} - } + {.type = selection_is_directional? kJsonTrue : kJsonFalse}, + {.type = kJsonNumber, .number_value = composing_base}, + {.type = kJsonNumber, .number_value = composing_extent} } } } }, - kJSONMethodCallResponse, NULL, NULL ); } -int textin_perform_action(enum text_input_action action) { - +int client_perform_action( + double connection_id, + enum text_input_action action +) { char *action_str = (action == kTextInputActionNone) ? "TextInputAction.none" : (action == kTextInputActionUnspecified) ? "TextInputAction.unspecified" : @@ -380,85 +615,195 @@ int textin_perform_action(enum text_input_action action) { (action == kTextInputActionEmergencyCall) ? "TextInputAction.emergencyCall" : "TextInputAction.newline"; - return platch_send( + return platch_call_json( TEXT_INPUT_CHANNEL, - &(struct platch_obj) { - .codec = kJSONMethodCall, - .method = "TextInputClient.performAction", - .json_arg = { - .type = kJsonArray, - .size = 2, - .array = (struct json_value[2]) { - {.type = kJsonNumber, .number_value = text_input.transaction_id}, - {.type = kJsonString, .string_value = action_str} + "TextInputClient.performAction", + &(struct json_value) { + .type = kJsonArray, + .size = 2, + .array = (struct json_value[2]) { + {.type = kJsonNumber, .number_value = connection_id}, + {.type = kJsonString, .string_value = action_str} + } + }, + NULL, + NULL + ); +} + +int client_perform_private_command( + double connection_id, + char *action, + struct json_value *data +) { + if (data != NULL && data->type != kJsonNull && data->type != kJsonObject) { + return EINVAL; + } + + return platch_call_json( + TEXT_INPUT_CHANNEL, + "TextInputClient.performPrivateCommand", + &(struct json_value) { + .type = kJsonArray, + .size = 2, + .array = (struct json_value[2]) { + {.type = kJsonNumber, .number_value = connection_id}, + { + .type = kJsonObject, + .size = 2, + .keys = (char*[2]) { + "action", + "data" + }, + .values = (struct json_value[2]) { + {.type = kJsonString, .string_value = action}, + *data + } } } }, - 0, NULL, NULL + NULL, + NULL ); } -int textin_on_connection_closed(void) { - text_input.transaction_id = -1; +int client_update_floating_cursor( + double connection_id, + enum floating_cursor_drag_state text_cursor_action, + double x, + double y +) { + return platch_call_json( + TEXT_INPUT_CHANNEL, + "TextInputClient.updateFloatingCursor", + &(struct json_value) { + .type = kJsonArray, + .size = 3, + .array = (struct json_value[3]) { + {.type = kJsonNumber, .number_value = connection_id}, + { + .type = kJsonString, + .string_value = text_cursor_action == kFloatingCursorDragStateStart ? "FloatingCursorDragState.start" : + text_cursor_action == kFloatingCursorDragStateUpdate ? "FloatingCursorDragState.update" : + "FloatingCursorDragState.end" + }, + { + .type = kJsonObject, + .size = 2, + .keys = (char*[2]) { + "X", + "Y" + }, + .values = (struct json_value[2]) { + {.type = kJsonNumber, .number_value = x}, + {.type = kJsonNumber, .number_value = y} + } + } + } + }, + NULL, + NULL + ); +} - return platch_send( +int client_on_connection_closed(double connection_id) { + return platch_call_json( TEXT_INPUT_CHANNEL, - &(struct platch_obj) { - .codec = kJSONMethodCall, - .method = "TextInputClient.onConnectionClosed", - .json_arg = {.type = kJsonNull} + "TextInputClient.onConnectionClosed", + &(struct json_value) { + .type = kJsonArray, + .size = 1, + .array = (struct json_value[1]) { + {.type = kJsonNumber, .number_value = connection_id} + } }, - kBinaryCodec, NULL, NULL + NULL, + NULL ); } -inline int to_byte_index(unsigned int symbol_index) { - char *cursor = text_input.text; +int client_show_autocorrection_prompt_rect( + double connection_id, + double start, + double end +) { + return platch_call_json( + TEXT_INPUT_CHANNEL, + "TextInputClient.showAutocorrectionPromptRect", + &(struct json_value) { + .type = kJsonArray, + .size = 3, + .array = (struct json_value[3]) { + {.type = kJsonNumber, .number_value = connection_id}, + {.type = kJsonNumber, .number_value = start}, + {.type = kJsonNumber, .number_value = end} + } + }, + NULL, + NULL + ); +} - while ((*cursor) && (symbol_index--)) - cursor += utf8_symbol_length(cursor); +/** + * Text Input Model functions. + */ - if (*cursor) - return cursor - text_input.text; +static inline int min(int a, int b) { + return a < b? a : b; +} - return -1; +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); +} + +static inline int selection_end(void) { + return max(text_input.selection_base, text_input.selection_extent); } -// start and end index are both inclusive. -int textin_erase(unsigned int start, unsigned int end) { +/** + * Erases the characters between `start` and `end` (both inclusive) and returns + * `start`. + */ +static int model_erase(unsigned int start, unsigned int end) { // 0 <= start <= end < len - char *start_str = utf8_symbol_at(text_input.text, start); - char *after_end_str = utf8_symbol_at(text_input.text, end+1); + char *start_str = symbol_at(start); + char *after_end_str = symbol_at(end+1); if (start_str && after_end_str) memmove(start_str, after_end_str, strlen(after_end_str) + 1 /* null byte */); return start; } -bool textin_delete_selected(void) { + +static bool model_delete_selected(void) { // erase selected text - text_input.selection_base = textin_erase(text_input.selection_base, text_input.selection_extent-1); + text_input.selection_base = model_erase(selection_start(), selection_end()-1); text_input.selection_extent = text_input.selection_base; return true; } -bool textin_add_utf8_char(char *c) { + +static bool model_add_utf8_char(uint8_t *c) { size_t symbol_length; - char *to_move; + uint8_t *to_move; if (text_input.selection_base != text_input.selection_extent) - textin_delete_selected(); + model_delete_selected(); // find out where in our string we need to insert the utf8 symbol - symbol_length = utf8_symbol_length(c); - to_move = utf8_symbol_at(text_input.text, text_input.selection_base); + symbol_length = utf8_symbol_length(*c); + to_move = symbol_at(text_input.selection_base); if (!to_move || !symbol_length) return false; // move the string behind the insertion position to - // make place for the utf8 character + // make place for the utf8 charactercursor memmove(to_move + symbol_length, to_move, strlen(to_move) + 1 /* null byte */); @@ -473,32 +818,35 @@ bool textin_add_utf8_char(char *c) { return true; } -bool textin_backspace(void) { + +static bool model_backspace(void) { if (text_input.selection_base != text_input.selection_extent) - return textin_delete_selected(); + return model_delete_selected(); if (text_input.selection_base != 0) { int base = text_input.selection_base - 1; - text_input.selection_base = textin_erase(base, base); + text_input.selection_base = model_erase(base, base); text_input.selection_extent = text_input.selection_base; return true; } return false; } -bool textin_delete(void) { + +static bool model_delete(void) { if (text_input.selection_base != text_input.selection_extent) - return textin_delete_selected(); + return model_delete_selected(); - if (text_input.selection_base < strlen(text_input.text)) { - text_input.selection_base = textin_erase(text_input.selection_base, text_input.selection_base); + if (selection_start() < strlen(text_input.text)) { + text_input.selection_base = model_erase(selection_start(), selection_end()); text_input.selection_extent = text_input.selection_base; return true; } return false; } -bool textin_move_cursor_to_beginning(void) { + +static bool model_move_cursor_to_beginning(void) { if ((text_input.selection_base != 0) || (text_input.selection_extent != 0)) { text_input.selection_base = 0; text_input.selection_extent = 0; @@ -507,8 +855,9 @@ bool textin_move_cursor_to_beginning(void) { return false; } -bool textin_move_cursor_to_end(void) { - int end = strlen(text_input.text); + +static bool model_move_cursor_to_end(void) { + int end = to_symbol_index(strlen(text_input.text)); if (text_input.selection_base != end) { text_input.selection_base = end; @@ -518,13 +867,14 @@ bool textin_move_cursor_to_end(void) { return false; } -bool textin_move_cursor_forward(void) { + +static bool model_move_cursor_forward(void) { if (text_input.selection_base != text_input.selection_extent) { text_input.selection_base = text_input.selection_extent; return true; } - if (text_input.selection_extent < strlen(text_input.text)) { + if (text_input.selection_extent < to_symbol_index(strlen(text_input.text))) { text_input.selection_extent++; text_input.selection_base++; return true; @@ -532,7 +882,8 @@ bool textin_move_cursor_forward(void) { return false; } -bool textin_move_cursor_back(void) { + +static bool model_move_cursor_back(void) { if (text_input.selection_base != text_input.selection_extent) { text_input.selection_extent = text_input.selection_base; return true; @@ -548,64 +899,104 @@ bool textin_move_cursor_back(void) { } -// these two functions automatically sync the editing state with flutter if -// a change ocurred, so you don't explicitly need to call textin_sync_editing_state(). -// `c` doesn't need to be NULL-terminated, the length of the char will be calculated -// using the start byte. -int textin_on_utf8_char(char *c) { - if (text_input.transaction_id == -1) + +static int sync_editing_state(void) { + return client_update_editing_state( + text_input.connection_id, + text_input.text, + text_input.selection_base, + text_input.selection_extent, + text_input.selection_affinity_is_downstream, + text_input.selection_is_directional, + text_input.composing_base, + text_input.composing_extent + ); +} + +/** + * `c` doesn't need to be NULL-terminated, the length of the char will be calculated + * using the start byte. + */ +int textin_on_utf8_char(uint8_t *c) { + if (text_input.connection_id == -1) return 0; + + switch (text_input.input_type) { + case kInputTypeNumber: + if (isdigit(*c)) { + break; + } else { + return 0; + } + case kInputTypePhone: + if (isdigit(*c) || *c == '*' || *c == '#' || *c == '+') { + break; + } else { + return 0; + } + default: + break; + } - if (textin_add_utf8_char(c)) - return textin_sync_editing_state(); + if (model_add_utf8_char(c)) + return sync_editing_state(); return 0; } -int textin_on_key(glfw_key key) { +int textin_on_xkb_keysym(xkb_keysym_t keysym) { bool needs_sync = false; bool perform_action = false; int ok; - if (text_input.transaction_id == -1) + if (text_input.connection_id == -1) return 0; - switch (key) { - case GLFW_KEY_LEFT: - needs_sync = textin_move_cursor_back(); - break; - case GLFW_KEY_RIGHT: - needs_sync = textin_move_cursor_forward(); - break; - case GLFW_KEY_END: - needs_sync = textin_move_cursor_to_end(); - break; - case GLFW_KEY_HOME: - needs_sync = textin_move_cursor_to_beginning(); + switch (keysym) { + case XKB_KEY_BackSpace: + needs_sync = model_backspace(); break; - case GLFW_KEY_BACKSPACE: - needs_sync = textin_backspace(); + case XKB_KEY_Delete: + case XKB_KEY_KP_Delete: + needs_sync = model_delete(); break; - case GLFW_KEY_DELETE: - needs_sync = textin_delete(); + case XKB_KEY_End: + case XKB_KEY_KP_End: + needs_sync = model_move_cursor_to_end(); break; - case GLFW_KEY_ENTER: + case XKB_KEY_Return: + case XKB_KEY_KP_Enter: + case XKB_KEY_ISO_Enter: if (text_input.input_type == kInputTypeMultiline) - needs_sync = textin_add_utf8_char("\n"); + needs_sync = model_add_utf8_char("\n"); perform_action = true; break; + case XKB_KEY_Home: + case XKB_KEY_KP_Home: + needs_sync = model_move_cursor_to_beginning(); + break; + case XKB_KEY_Left: + case XKB_KEY_KP_Left: + // handled inside of flutter + // needs_sync = model_move_cursor_back(); + break; + case XKB_KEY_Right: + case XKB_KEY_KP_Right: + // handled inside of flutter + // needs_sync = model_move_cursor_forward(); + break; default: break; } if (needs_sync) { - ok = textin_sync_editing_state(); + ok = sync_editing_state(); if (ok != 0) return ok; } if (perform_action) { - ok = textin_perform_action(text_input.input_action); + ok = client_perform_action(text_input.connection_id, text_input.input_action); if (ok != 0) return ok; } @@ -619,7 +1010,7 @@ int textin_init(void) { text_input.text[0] = '\0'; text_input.warned_about_autocorrect = false; - ok = plugin_registry_set_receiver(TEXT_INPUT_CHANNEL, kJSONMethodCall, textin_on_receive); + ok = plugin_registry_set_receiver(TEXT_INPUT_CHANNEL, kJSONMethodCall, on_receive); if (ok != 0) return ok; return 0;