diff --git a/.gitignore b/.gitignore index b755a331..a8d90620 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ out .vscode -build.sh \ No newline at end of file +build.sh +compile_commands.json +.clang-format \ No newline at end of file diff --git a/Makefile b/Makefile index d9ce7357..91619993 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,11 @@ CC = cc LD = cc -REAL_CFLAGS = -I./include $(shell pkg-config --cflags dri gbm libdrm glesv2 egl) -DBUILD_ELM327PLUGIN $(CFLAGS) +REAL_CFLAGS = -I./include $(shell pkg-config --cflags dri gbm libdrm glesv2 egl) -DBUILD_ELM327_PLUGIN -DBUILD_TEST_PLUGIN -ggdb $(CFLAGS) REAL_LDFLAGS = $(shell pkg-config --libs dri gbm libdrm glesv2 egl) -lrt -lflutter_engine -lpthread -ldl $(LDFLAGS) -SOURCES = src/flutter-pi.c src/platformchannel.c src/pluginregistry.c src/plugins/elm327plugin.c src/plugins/services-plugin.c src/plugins/testplugin.c +SOURCES = src/flutter-pi.c src/platformchannel.c src/pluginregistry.c src/console_keyboard.c \ + src/plugins/elm327plugin.c src/plugins/services-plugin.c src/plugins/testplugin.c src/plugins/text_input.c \ + src/plugins/raw_keyboard.c OBJECTS = $(patsubst src/%.c,out/obj/%.o,$(SOURCES)) all: out/flutter-pi diff --git a/include/console_keyboard.h b/include/console_keyboard.h new file mode 100644 index 00000000..b3b70f41 --- /dev/null +++ b/include/console_keyboard.h @@ -0,0 +1,191 @@ +#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 f23f3866..4c11dddc 100644 --- a/include/flutter-pi.h +++ b/include/flutter-pi.h @@ -74,6 +74,7 @@ struct mousepointer_mtslot { FlutterPointerPhase phase; }; + #define INPUT_BUSTYPE_FRIENDLY_NAME(bustype) ( \ (bustype) == BUS_PCI ? "PCI/e" : \ (bustype) == BUS_USB ? "USB" : \ @@ -91,6 +92,14 @@ struct mousepointer_mtslot { (code) == BTN_BACK ? kFlutterPointerButtonMouseBack : \ (code) == BTN_TOUCH ? (1 << 8) : 0) +#define MODIFIER_KEY_FROM_EVENT_CODE(code) ((uint16_t) \ + ((code) == KEY_LEFTCTRL) || ((code) == KEY_RIGHTCTRL) ? kControlModifier : \ + ((code) == KEY_LEFTSHIFT) || ((code) == KEY_RIGHTSHIFT) ? kShiftModifier : \ + ((code) == KEY_LEFTALT) || ((code) == KEY_RIGHTALT) ? kAltModifier : \ + ((code) == KEY_LEFTMETA) || ((code) == KEY_RIGHTMETA) ? kMetaModifier : \ + ((code) == KEY_CAPSLOCK) ? kCapsLockModifier : \ + ((code) == KEY_NUMLOCK) ? kNumLockModifier : 0) + #define POINTER_PHASE_AS_STRING(phase) ( \ (phase) == kCancel ? "kCancel" : \ (phase) == kUp ? "kUp" : \ @@ -126,7 +135,6 @@ struct input_device { size_t n_mtslots; size_t i_active_mtslot; struct mousepointer_mtslot *mtslots; - //struct mousepointer_mtslot *active_mtslot; // currently pressed buttons (for mouse, touchpad, stylus) // (active_buttons & 0xFF) will be the value of the "buttons" field diff --git a/include/platformchannel.h b/include/platformchannel.h index f454dd59..96105b30 100644 --- a/include/platformchannel.h +++ b/include/platformchannel.h @@ -193,6 +193,9 @@ typedef int (*PlatformMessageResponseCallback)(struct ChannelObject *object, voi /// you'd have to manually deep-copy it. int PlatformChannel_decode(uint8_t *buffer, size_t size, enum ChannelCodec codec, struct ChannelObject *object_out); +/// decodes a JSON String into a JSONMsgCodecValue +int PlatformChannel_decodeJSON(char *string, struct JSONMsgCodecValue *out); + /// Encodes a generic ChannelObject into a buffer (that is, too, allocated by PlatformChannel_encode) /// A pointer to the buffer is put into buffer_out and the size of that buffer into size_out. /// The lifetime of the buffer is independent of the ChannelObject, so contents of the ChannelObject @@ -246,9 +249,11 @@ int PlatformChannel_respondError(FlutterPlatformMessageResponseHandle *handle, e /// not freeing ChannelObjects may result in a memory leak. int PlatformChannel_free(struct ChannelObject *object); +int PlatformChannel_freeJSONMsgCodecValue(struct JSONMsgCodecValue *value, bool shallow); + /// returns true if values a and b are equal. /// for JS arrays, the order of the values is relevant -/// (so two arrays are only equal if the same values in appear in exactly same order) +/// (so two arrays are only equal if the same values appear in exactly same order) /// for objects, the order of the entries is irrelevant. bool jsvalue_equals(struct JSONMsgCodecValue *a, struct JSONMsgCodecValue *b); diff --git a/src/console_keyboard.c b/src/console_keyboard.c new file mode 100644 index 00000000..a2506271 --- /dev/null +++ b/src/console_keyboard.c @@ -0,0 +1,277 @@ +#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; +} + +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 16f24751..293c0d3d 100644 --- a/src/flutter-pi.c +++ b/src/flutter-pi.c @@ -1,5 +1,6 @@ #define _GNU_SOURCE +#include #include #include #include @@ -32,8 +33,12 @@ #include #include +#include #include #include +#include "plugins/services-plugin.h" +#include "plugins/text_input.h" +#include "plugins/raw_keyboard.h" char* usage ="\ @@ -484,7 +489,7 @@ bool message_loop(void) { pthread_mutex_lock(&tasklist_lock); // wait for a task to be inserted into the list - while ((tasklist.next == NULL)) + while (tasklist.next == NULL) pthread_cond_wait(&task_added, &tasklist_lock); // wait for a task to be ready to be run @@ -537,7 +542,7 @@ bool message_loop(void) { orientation = task->orientation; // send updated window metrics to flutter - FlutterEngineResult result = FlutterEngineSendWindowMetricsEvent(engine, &(const FlutterWindowMetricsEvent) { + FlutterEngineSendWindowMetricsEvent(engine, &(const FlutterWindowMetricsEvent) { .struct_size = sizeof(FlutterWindowMetricsEvent), // we send swapped width/height if the screen is rotated 90 or 270 degrees. @@ -622,9 +627,9 @@ bool init_display(void) { * DRM INITIALIZATION * **********************/ - drmModeRes *resources; + drmModeRes *resources = NULL; drmModeConnector *connector; - drmModeEncoder *encoder; + drmModeEncoder *encoder = NULL; int i, ok, area; if (!drm.has_device) { @@ -700,7 +705,7 @@ bool init_display(void) { printf(" flutter-pi chose \"%s\" as its DRM device.\n", device->nodes[DRM_NODE_PRIMARY]); drm.fd = fd; drm.has_device = true; - snprintf(drm.device, sizeof(drm.device)-1, device->nodes[DRM_NODE_PRIMARY]); + snprintf(drm.device, sizeof(drm.device)-1, "%s", device->nodes[DRM_NODE_PRIMARY]); } if (!drm.has_device) { @@ -870,9 +875,7 @@ bool init_display(void) { gbm.surface = NULL; gbm.modifier = DRM_FORMAT_MOD_LINEAR; - if (gbm_surface_create_with_modifiers) { - gbm.surface = gbm_surface_create_with_modifiers(gbm.device, width, height, gbm.format, &gbm.modifier, 1); - } + gbm.surface = gbm_surface_create_with_modifiers(gbm.device, width, height, gbm.format, &gbm.modifier, 1); if (!gbm.surface) { if (gbm.modifier != DRM_FORMAT_MOD_LINEAR) { @@ -935,7 +938,7 @@ bool init_display(void) { egl.modifiers_supported = strstr(egl_exts_dpy, "EGL_EXT_image_dma_buf_import_modifiers") != NULL; - printf("Using display %d with EGL version %d.%d\n", egl.display, major, minor); + printf("Using display %p with EGL version %d.%d\n", egl.display, major, minor); printf("===================================\n"); printf("EGL information:\n"); printf(" version: %s\n", eglQueryString(egl.display, EGL_VERSION)); @@ -1195,7 +1198,6 @@ void init_io(void) { .device = 0, .buttons = 0 }; - // go through all the given paths and add everything you can for (int i=0; i < input_devices_glob.gl_pathc; i++) { @@ -1224,7 +1226,7 @@ void init_io(void) { } printf(" %s, connected via %s. vendor: 0x%04X, product: 0x%04X, version: 0x%04X\n", dev.name, - INPUT_BUSTYPE_FRIENDLY_NAME(dev.input_id.bustype), dev.input_id.vendor, dev.input_id.vendor); + INPUT_BUSTYPE_FRIENDLY_NAME(dev.input_id.bustype), dev.input_id.vendor, dev.input_id.product, dev.input_id.version); // query supported event codes (for EV_ABS, EV_REL and EV_KEY event types) ok = ioctl(dev.fd, EVIOCGBIT(EV_ABS, sizeof(absbits)), absbits); @@ -1315,14 +1317,21 @@ void init_io(void) { } if (n_input_devices == 0) - printf("Warning: No input devices configured.\n"); + printf("Warning: No evdev input devices configured.\n"); + // configure the console + ok = console_make_raw(); + if (ok != 0) { + printf("[flutter-pi] warning: could not make stdin raw\n"); + } + + console_flush_stdin(); + // now send all the kAdd events to flutter. ok = kSuccess == FlutterEngineSendPointerEvent(engine, flutterevents, i_flutterevent); if (!ok) fprintf(stderr, "error while sending initial mousepointer / multitouch slot information to flutter\n"); - i_flutterevent = 0; } -void on_user_input(fd_set fds, size_t n_ready_fds) { +void on_evdev_input(fd_set fds, size_t n_ready_fds) { struct input_event linuxevents[64]; size_t n_linuxevents; struct input_device *device; @@ -1430,7 +1439,19 @@ void on_user_input(fd_set fds, size_t n_ready_fds) { // update the active_buttons bitmap // only apply BTN_TOUCH to the active buttons if a touch really equals a pressed button (device->is_direct is set) // is_direct is true for touchscreens, but not for touchpads; so BTN_TOUCH doesn't result in a kMove for touchpads - if (e->code != BTN_TOUCH || device->is_direct) { + + glfw_key glfw_key = EVDEV_KEY_TO_GLFW_KEY(e->code); + if ((glfw_key != GLFW_KEY_UNKNOWN) && (glfw_key != 0)) { + glfw_key_action action; + switch (e->value) { + case 0: action = GLFW_RELEASE; break; + case 1: action = GLFW_PRESS; break; + case 2: action = GLFW_REPEAT; break; + default: action = -1; break; + } + + RawKeyboard_onKeyEvent(EVDEV_KEY_TO_GLFW_KEY(e->code), 0, action); + } else if (e->code != BTN_TOUCH || device->is_direct) { if (e->value == 1) device->active_buttons |= FLUTTER_BUTTON_FROM_EVENT_CODE(e->code); else device->active_buttons &= ~FLUTTER_BUTTON_FROM_EVENT_CODE(e->code); } @@ -1445,7 +1466,7 @@ void on_user_input(fd_set fds, size_t n_ready_fds) { // We can now summarise the updates we received from the evdev into a FlutterPointerEvent // and put it in the flutterevents buffer. - size_t n_slots; + size_t n_slots = 0; struct mousepointer_mtslot *slots; // if this is a pointer device, we don't care about the multitouch slots & only send the updated mousepointer. @@ -1504,8 +1525,37 @@ void on_user_input(fd_set fds, size_t n_ready_fds) { if (!ok) { fprintf(stderr, "could not send pointer events to flutter engine\n"); } +} +void on_console_input(void) { + static char buffer[4096]; + glfw_key key; + char *cursor; + char *c; + int ok; - i_flutterevent = 0; + ok = read(STDIN_FILENO, buffer, sizeof(buffer)); + if (ok == -1) { + perror("could not read from stdin"); + return; + } else if (ok == 0) { + fprintf(stderr, "warning: reached EOF for stdin\n"); + return; + } + + buffer[ok] = '\0'; + + cursor = buffer; + while (*cursor) { + if (key = console_try_get_key(cursor, &cursor), key != GLFW_KEY_UNKNOWN) { + TextInput_onKey(key); + } else if (c = console_try_get_utf8char(cursor, &cursor), c != NULL) { + TextInput_onUtf8Char(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; + } + } } void *io_loop(void *userdata) { int n_ready_fds; @@ -1524,6 +1574,8 @@ void *io_loop(void *userdata) { FD_SET(drm.fd, &fds); if (drm.fd + 1 > nfds) nfds = drm.fd + 1; + + FD_SET(STDIN_FILENO, &fds); const fd_set const_fds = fds; @@ -1545,9 +1597,15 @@ void *io_loop(void *userdata) { FD_CLR(drm.fd, &fds); n_ready_fds--; } + + if (FD_ISSET(STDIN_FILENO, &fds)) { + on_console_input(); + FD_CLR(STDIN_FILENO, &fds); + n_ready_fds--; + } if (n_ready_fds > 0) { - on_user_input(fds, n_ready_fds); + on_evdev_input(fds, n_ready_fds); } fds = const_fds; diff --git a/src/platformchannel.c b/src/platformchannel.c index dc899c79..d0522f85 100644 --- a/src/platformchannel.c +++ b/src/platformchannel.c @@ -290,9 +290,29 @@ size_t PlatformChannel_calculateJSONMsgCodecValueSize(struct JSONMsgCodecValue * return 5; case kJSNumber: ; char numBuffer[32]; - return sprintf(numBuffer, "%lf", value->number_value); + return sprintf(numBuffer, "%g", value->number_value); case kJSString: - return strlen(value->string_value) +2; + size = 2; + + // we need to count how many characters we need to escape. + for (char *s = value->string_value; *s; s++) { + switch (*s) { + case '\b': + case '\f': + case '\n': + case '\r': + case '\t': + case '\"': + case '\\': + size += 2; + break; + default: + size++; + break; + } + } + + return size; case kJSArray: size += 2; for (int i=0; i < value->size; i++) { @@ -325,10 +345,49 @@ int PlatformChannel_writeJSONMsgCodecValueToBuffer(struct JSONMsgCodecValue* val *pbuffer += sprintf((char*) *pbuffer, "false"); break; case kJSNumber: - *pbuffer += sprintf((char*) *pbuffer, "%lf", value->number_value); + *pbuffer += sprintf((char*) *pbuffer, "%g", value->number_value); break; case kJSString: - *pbuffer += sprintf((char*) *pbuffer, "\"%s\"", value->string_value); + *((*pbuffer)++) = '\"'; + + for (char *s = value->string_value; *s; s++) { + switch (*s) { + case '\b': + *((*pbuffer)++) = '\\'; + *((*pbuffer)++) = 'b'; + break; + case '\f': + *((*pbuffer)++) = '\\'; + *((*pbuffer)++) = 'f'; + break; + case '\n': + *((*pbuffer)++) = '\\'; + *((*pbuffer)++) = 'n'; + break; + case '\r': + *((*pbuffer)++) = '\\'; + *((*pbuffer)++) = 'r'; + break; + case '\t': + *((*pbuffer)++) = '\\'; + *((*pbuffer)++) = 't'; + break; + case '\"': + *((*pbuffer)++) = '\\'; + *((*pbuffer)++) = 't'; + break; + case '\\': + *((*pbuffer)++) = '\\'; + *((*pbuffer)++) = '\\'; + break; + default: + *((*pbuffer)++) = *s; + break; + } + } + + *((*pbuffer)++) = '\"'; + break; case kJSArray: *pbuffer += sprintf((char*) *pbuffer, "["); @@ -585,6 +644,10 @@ int PlatformChannel_decodeJSONMsgCodecValue(char *message, size_t size, jsmntok_ return 0; } +int PlatformChannel_decodeJSON(char *string, struct JSONMsgCodecValue *out) { + return PlatformChannel_decodeJSONMsgCodecValue(string, strlen(string), NULL, NULL, out); +} + int PlatformChannel_decode(uint8_t *buffer, size_t size, enum ChannelCodec codec, struct ChannelObject *object_out) { struct JSONMsgCodecValue root_jsvalue; uint8_t *buffer_cursor = buffer; diff --git a/src/pluginregistry.c b/src/pluginregistry.c index f1178117..806dca81 100644 --- a/src/pluginregistry.c +++ b/src/pluginregistry.c @@ -6,8 +6,15 @@ // hardcoded plugin headers #include "plugins/services-plugin.h" +#include "plugins/text_input.h" +#include "plugins/raw_keyboard.h" + +#ifdef BUILD_TEST_PLUGIN #include "plugins/testplugin.h" +#endif +#ifdef BUILD_ELM327_PLUGIN #include "plugins/elm327plugin.h" +#endif struct ChannelObjectReceiverData { @@ -25,13 +32,15 @@ struct { /// array of plugins that are statically included in flutter-pi. struct FlutterPiPlugin hardcoded_plugins[] = { {.name = "services", .init = Services_init, .deinit = Services_deinit}, + {.name = "text_input", .init = TextInput_init, .deinit = TextInput_deinit}, + {.name = "raw_keyboard", .init = RawKeyboard_init, .deinit = RawKeyboard_deinit}, -#ifdef BUILD_TESTPLUGIN - {.name = "testplugin", .init = TestPlugin_init, .deinit = TestPlugin_deinit} +#ifdef BUILD_TEST_PLUGIN + {.name = "testplugin", .init = TestPlugin_init, .deinit = TestPlugin_deinit}, #endif -#ifdef BUILD_ELM327PLUGIN - {.name = "elm327plugin", .init = ELM327Plugin_init, .deinit = ELM327Plugin_deinit} +#ifdef BUILD_ELM327_PLUGIN + {.name = "elm327plugin", .init = ELM327Plugin_init, .deinit = ELM327Plugin_deinit}, #endif }; //size_t hardcoded_plugins_count; diff --git a/src/plugins/elm327plugin.c b/src/plugins/elm327plugin.c index a58b0301..fab968b9 100644 --- a/src/plugins/elm327plugin.c +++ b/src/plugins/elm327plugin.c @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/plugins/elm327plugin.h b/src/plugins/elm327plugin.h index 2f008c76..eae0e410 100644 --- a/src/plugins/elm327plugin.h +++ b/src/plugins/elm327plugin.h @@ -1,7 +1,6 @@ -#ifndef ELM327PLUGIN_H -#define ELM327PLUGIN_H +#ifndef _ELM327_PLUGIN_H +#define _ELM327_PLUGIN_H -#include #include #include #include diff --git a/src/plugins/raw_keyboard.c b/src/plugins/raw_keyboard.c new file mode 100644 index 00000000..6786b27c --- /dev/null +++ b/src/plugins/raw_keyboard.c @@ -0,0 +1,132 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include "raw_keyboard.h" + +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 = {.initialized = false}; + +int RawKeyboard_sendGlfwKeyEvent(uint32_t code_point, glfw_key key_code, uint32_t scan_code, glfw_keymod_map mods, bool is_down) { + return PlatformChannel_send( + KEY_EVENT_CHANNEL, + &(struct ChannelObject) { + .codec = kJSONMessageCodec, + .jsonmsgcodec_value = { + .type = kJSObject, + .size = 7, + .keys = (char*[7]) { + "keymap", "toolkit", "unicodeScalarValues", "keyCode", "scanCode", + "modifiers", "type" + }, + .values = (struct JSONMsgCodecValue[7]) { + {.type = kJSString, .string_value = "linux"}, + {.type = kJSString, .string_value = "glfw"}, + {.type = kJSNumber, .number_value = code_point}, + {.type = kJSNumber, .number_value = key_code}, + {.type = kJSNumber, .number_value = scan_code}, + {.type = kJSNumber, .number_value = mods}, + {.type = kJSString, .string_value = is_down? "keydown" : "keyup"} + } + } + }, + kJSONMessageCodec, + NULL, + NULL + ); +} + +int RawKeyboard_onKeyEvent(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; + + if (action == GLFW_PRESS) { + lrmods_after |= sided_mod; + } else if (action == GLFW_RELEASE) { + lrmods_after &= ~sided_mod; + } + 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) { + RawKeyboard_sendGlfwKeyEvent(0, key, scan_code, raw_keyboard.mods, action != GLFW_RELEASE); + } + + raw_keyboard.leftright_mods = lrmods_after; + raw_keyboard.mods = mods_after; +} + +int RawKeyboard_init(void) { + raw_keyboard.leftright_mods = 0; + raw_keyboard.mods = 0; + raw_keyboard.initialized = true; + + printf("[raw_keyboard] init.\n"); + return 0; +} + +int RawKeyboard_deinit(void) { + raw_keyboard.initialized = false; + + printf("[raw_keyboard] deinit.\n"); + return 0; +} \ No newline at end of file diff --git a/src/plugins/raw_keyboard.h b/src/plugins/raw_keyboard.h new file mode 100644 index 00000000..f9fb3fc8 --- /dev/null +++ b/src/plugins/raw_keyboard.h @@ -0,0 +1,11 @@ +#ifndef _KEY_EVENT_H +#define _KEY_EVENT_H + +#define KEY_EVENT_CHANNEL "flutter/keyevent" + +int RawKeyboard_onKeyEvent(glfw_key key, uint32_t scan_code, glfw_key_action action); + +int RawKeyboard_init(void); +int RawKeyboard_deinit(void); + +#endif \ No newline at end of file diff --git a/src/plugins/services-plugin.c b/src/plugins/services-plugin.c index 8a4c5bdc..dc4fea39 100644 --- a/src/plugins/services-plugin.c +++ b/src/plugins/services-plugin.c @@ -9,7 +9,7 @@ struct { char label[256]; uint32_t primaryColor; // ARGB8888 (blue is the lowest byte) char isolateId[32]; -} ServicesPlugin = {0}; +} services = {0}; int Services_onReceiveNavigation(char *channel, struct ChannelObject *object, FlutterPlatformMessageResponseHandle *responsehandle) { @@ -17,8 +17,8 @@ int Services_onReceiveNavigation(char *channel, struct ChannelObject *object, Fl } int Services_onReceiveIsolate(char *channel, struct ChannelObject *object, FlutterPlatformMessageResponseHandle *responsehandle) { - memset(&(ServicesPlugin.isolateId), sizeof(ServicesPlugin.isolateId), 0); - memcpy(ServicesPlugin.isolateId, object->binarydata, object->binarydata_size); + memset(&(services.isolateId), sizeof(services.isolateId), 0); + memcpy(services.isolateId, object->binarydata, object->binarydata_size); return PlatformChannel_respondNotImplemented(responsehandle); } @@ -129,7 +129,7 @@ int Services_onReceivePlatform(char *channel, struct ChannelObject *object, Flut value = jsobject_get(arg, "label"); if (value && (value->type == kJSString)) - snprintf(ServicesPlugin.label, sizeof(ServicesPlugin.label), "%s", value->string_value); + snprintf(services.label, sizeof(services.label), "%s", value->string_value); return PlatformChannel_respond(responsehandle, &(struct ChannelObject) { .codec = kJSONMethodCallResponse, @@ -180,36 +180,37 @@ int Services_onReceiveAccessibility(char *channel, struct ChannelObject *object, } + int Services_init(void) { int ok; + printf("[services-plugin] init.\n"); + ok = PluginRegistry_setReceiver("flutter/navigation", kJSONMethodCall, Services_onReceiveNavigation); if (ok != 0) { - printf("Could not set flutter/navigation ChannelObject receiver: %s\n", strerror(ok)); + fprintf(stderr, "[services-plugin] could not set \"flutter/navigation\" ChannelObject receiver: %s\n", strerror(ok)); return ok; } ok = PluginRegistry_setReceiver("flutter/isolate", kBinaryCodec, Services_onReceiveIsolate); if (ok != 0) { - printf("Could not set flutter/isolate ChannelObject receiver: %s\n", strerror(ok)); + fprintf(stderr, "[services-plugin] could not set \"flutter/isolate\" ChannelObject receiver: %s\n", strerror(ok)); return ok; } ok = PluginRegistry_setReceiver("flutter/platform", kJSONMethodCall, Services_onReceivePlatform); if (ok != 0) { - printf("Could not set flutter/platform ChannelObject receiver: %s\n", strerror(ok)); + fprintf(stderr, "[services-plugin] could not set \"flutter/platform\" ChannelObject receiver: %s\n", strerror(ok)); return ok; } ok = PluginRegistry_setReceiver("flutter/accessibility", kBinaryCodec, Services_onReceiveAccessibility); if (ok != 0) { - printf("Could not set flutter/accessibility ChannelObject receiver: %s\n", strerror(ok)); + fprintf(stderr, "[services-plugin] could not set \"flutter/accessibility\" ChannelObject receiver: %s\n", strerror(ok)); return ok; } - - printf("Initialized Services plugin.\n"); } int Services_deinit(void) { - printf("Deinitialized Services plugin.\n"); + printf("[services-plugin] deinit.\n"); } \ No newline at end of file diff --git a/src/plugins/text_input.c b/src/plugins/text_input.c new file mode 100644 index 00000000..ac6ff83c --- /dev/null +++ b/src/plugins/text_input.c @@ -0,0 +1,674 @@ +#include +#include +#include +#include +#include + +#include +#include +#include "text_input.h" + +struct { + int32_t transaction_id; + enum text_input_type input_type; + bool autocorrect; + enum text_input_action input_action; + char text[TEXT_INPUT_MAX_CHARS]; + int selection_base, selection_extent; + bool selection_affinity_is_downstream; + bool selection_is_directional; + int composing_base, composing_extent; + bool warned_about_autocorrect; +} text_input = { + .transaction_id = -1 +}; + +int TextInput_onReceive(char *channel, struct ChannelObject *object, FlutterPlatformMessageResponseHandle *responsehandle) { + struct JSONMsgCodecValue jsvalue, *temp, *temp2, *state, *config; + int ok; + + printf("[text_input] got method call: %s\n", object->method); + + 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->jsarg.type != kJSArray) || (object->jsarg.size != 2)) { + return PlatformChannel_respondError( + responsehandle, + kJSONMethodCallResponse, + "illegalargument", + "Expected JSON Array with length 2 as the argument.", + NULL + ); + } + + if (object->jsarg.array[0].type != kJSNumber) { + return PlatformChannel_respondError( + responsehandle, + kJSONMethodCallResponse, + "illegalargument", + "Expected transaction id to be a number.", + NULL + ); + } + + if (object->jsarg.array[1].type != kJSObject) { + return PlatformChannel_respondError( + responsehandle, + kJSONMethodCallResponse, + "illegalargument", + "Expected text input configuration to be a String", + NULL + ); + } + + struct JSONMsgCodecValue *config = &object->jsarg.array[1]; + + if (config->type != kJSObject) { + return PlatformChannel_respondError( + responsehandle, + kJSONMethodCallResponse, + "illegalargument", + "Expected decoded text input configuration to be an Object", + NULL + ); + } + + enum text_input_type input_type; + bool autocorrect; + enum text_input_action input_action; + + // AUTOCORRECT + temp = jsobject_get(config, "autocorrect"); + if (!(temp && ((temp->type == kJSTrue) || (temp->type == kJSFalse)))) + goto invalid_config; + + autocorrect = temp->type == kJSTrue; + + // INPUT ACTION + temp = jsobject_get(config, "inputAction"); + if (!(temp && (temp->type == kJSString))) + 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 != kJSObject) + goto invalid_config; + + + temp2 = jsobject_get(temp, "name"); + + if (!temp2 || temp2->type != kJSString) + 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; + } + + // TRANSACTION ID + int32_t new_id = (int32_t) object->jsarg.array[0].number_value; + + // 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; + + 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 PlatformChannel_respond( + responsehandle, + &(struct ChannelObject) { + .codec = kJSONMethodCallResponse, + .success = true, + .jsresult = {.type = kJSNull} + } + ); + + // invalid config given to setClient + invalid_config: + return PlatformChannel_respondError( + responsehandle, + kJSONMethodCallResponse, + "illegalargument", + "Expected decoded text input configuration to at least contain values for \"autocorrect\"" + " and \"inputAction\"", + NULL + ); + + } else if STREQ("TextInput.show", object->method) { + /* + * TextInput.show() + * Show the keyboard. See [TextInputConnection.show]. + * + */ + + // do nothing since we use a physical keyboard. + return PlatformChannel_respond( + responsehandle, + &(struct ChannelObject) { + .codec = kJSONMethodCallResponse, + .success = true, + .jsresult = {.type = kJSNull} + } + ); + } 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->jsarg; + + if (state->type != kJSObject) { + return PlatformChannel_respondError( + responsehandle, + kJSONMethodCallResponse, + "illegalargument", + "Expected decoded text editing value to be an Object", + NULL + ); + } + + char *text; + int selection_base, selection_extent, composing_base, composing_extent; + bool selection_affinity_is_downstream, selection_is_directional; + + temp = jsobject_get(state, "text"); + if (temp && (temp->type == kJSString)) text = temp->string_value; + else goto invalid_editing_value; + + temp = jsobject_get(state, "selectionBase"); + if (temp && (temp->type == kJSNumber)) selection_base = (int) temp->number_value; + else goto invalid_editing_value; + + temp = jsobject_get(state, "selectionExtent"); + if (temp && (temp->type == kJSNumber)) selection_extent = (int) temp->number_value; + else goto invalid_editing_value; + + temp = jsobject_get(state, "selectionAffinity"); + if (temp && (temp->type == kJSString)) { + 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; + } + + temp = jsobject_get(state, "selectionIsDirectional"); + if (temp && (temp->type == kJSTrue || temp->type == kJSFalse)) { + selection_is_directional = temp->type == kJSTrue; + } else { + goto invalid_editing_value; + } + + temp = jsobject_get(state, "composingBase"); + if (temp && (temp->type == kJSNumber)) composing_base = (int) temp->number_value; + else goto invalid_editing_value; + + temp = jsobject_get(state, "composingExtent"); + if (temp && (temp->type == kJSNumber)) composing_extent = (int) temp->number_value; + else goto invalid_editing_value; + + + // text editing value seems to be valid. + // apply it. + printf("[text_input] TextInput.setEditingState\n" + " text = \"%s\",\n" + " selectionBase = %i, selectionExtent = %i, selectionAffinity = %s\n" + " selectionIsDirectional = %s, composingBase = %i, composingExtent = %i\n", + text, selection_base, selection_extent, + selection_affinity_is_downstream? "downstream" : "upstream", + selection_is_directional? "true" : "false", composing_base, composing_extent + ); + + 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; + + return PlatformChannel_respond( + responsehandle, + &(struct ChannelObject) { + .codec = kJSONMethodCallResponse, + .success = true, + .jsresult = {.type = kJSNull} + } + ); + + invalid_editing_value: + return PlatformChannel_respondError( + responsehandle, + kJSONMethodCallResponse, + "illegalargument", + "Expected decoded text editing value to be a valid" + " JSON representation of a text editing value", + NULL + ); + + } 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; + + return PlatformChannel_respond( + responsehandle, + &(struct ChannelObject) { + .codec = kJSONMethodCallResponse, + .success = true, + .jsresult = {.type = kJSNull} + } + ); + } 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 PlatformChannel_respond( + responsehandle, + &(struct ChannelObject) { + .codec = kJSONMethodCallResponse, + .success = true, + .jsresult = {.type = kJSNull} + } + ); + } + + return PlatformChannel_respondNotImplemented(responsehandle); +} + + +int TextInput_syncEditingState() { + printf("[text_input] TextInputClient.updateEditingState\n" + " text = \"%s\",\n" + " selectionBase = %i, selectionExtent = %i, selectionAffinity = %s\n" + " selectionIsDirectional = %s, composingBase = %i, composingExtent = %i\n", + text_input.text, text_input.selection_base, text_input.selection_extent, + text_input.selection_affinity_is_downstream? "downstream" : "upstream", + text_input.selection_is_directional? "true" : "false", text_input.composing_base, text_input.composing_extent + ); + + return PlatformChannel_send( + TEXT_INPUT_CHANNEL, + &(struct ChannelObject) { + .codec = kJSONMethodCall, + .method = "TextInputClient.updateEditingState", + .jsarg = { + .type = kJSArray, + .size = 2, + .array = (struct JSONMsgCodecValue[2]) { + {.type = kJSNumber, .number_value = text_input.transaction_id}, + {.type = kJSObject, .size = 7, + .keys = (char*[7]) { + "text", "selectionBase", "selectionExtent", "selectionAffinity", + "selectionIsDirectional", "composingBase", "composingExtent" + }, + .values = (struct JSONMsgCodecValue[7]) { + {.type = kJSString, .string_value = text_input.text}, + {.type = kJSNumber, .number_value = text_input.selection_base}, + {.type = kJSNumber, .number_value = text_input.selection_extent}, + { + .type = kJSString, + .string_value = text_input.selection_affinity_is_downstream ? + "TextAffinity.downstream" : "TextAffinity.upstream" + }, + {.type = text_input.selection_is_directional? kJSTrue : kJSFalse}, + {.type = kJSNumber, .number_value = text_input.composing_base}, + {.type = kJSNumber, .number_value = text_input.composing_extent} + } + } + } + } + }, + kJSONMethodCallResponse, + NULL, + NULL + ); +} + +int TextInput_performAction(enum text_input_action action) { + + char *action_str = + (action == kTextInputActionNone) ? "TextInputAction.none" : + (action == kTextInputActionUnspecified) ? "TextInputAction.unspecified" : + (action == kTextInputActionDone) ? "TextInputAction.done" : + (action == kTextInputActionGo) ? "TextInputAction.go" : + (action == kTextInputActionSearch) ? "TextInputAction.search" : + (action == kTextInputActionSend) ? "TextInputAction.send" : + (action == kTextInputActionNext) ? "TextInputAction.next" : + (action == kTextInputActionPrevious) ? "TextInputAction.previous" : + (action == kTextInputActionContinueAction) ? "TextInputAction.continueAction" : + (action == kTextInputActionJoin) ? "TextInputAction.join" : + (action == kTextInputActionRoute) ? "TextInputAction.route" : + (action == kTextInputActionEmergencyCall) ? "TextInputAction.emergencyCall" : + "TextInputAction.newline"; + + return PlatformChannel_send( + TEXT_INPUT_CHANNEL, + &(struct ChannelObject) { + .codec = kJSONMethodCall, + .method = "TextInputClient.performAction", + .jsarg = { + .type = kJSArray, + .size = 2, + .array = (struct JSONMsgCodecValue[2]) { + {.type = kJSNumber, .number_value = text_input.transaction_id}, + {.type = kJSString, .string_value = action_str} + } + } + }, + 0, NULL, NULL + ); +} + +int TextInput_onConnectionClosed(void) { + text_input.transaction_id = -1; + + return PlatformChannel_send( + TEXT_INPUT_CHANNEL, + &(struct ChannelObject) { + .codec = kJSONMethodCall, + .method = "TextInputClient.onConnectionClosed", + .jsarg = {.type = kJSNull} + }, + kBinaryCodec, NULL, NULL + ); +} + +inline int to_byte_index(unsigned int symbol_index) { + char *cursor = text_input.text; + + while ((*cursor) && (symbol_index--)) + cursor += utf8_symbol_length(cursor); + + if (*cursor) + return cursor - text_input.text; + + return -1; +} + +// start and end index are both inclusive. +int TextInput_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); + + if (start_str && after_end_str) + memmove(start_str, after_end_str, strlen(after_end_str) + 1 /* null byte */); + + return start; +} +bool TextInput_deleteSelected(void) { + // erase selected text + text_input.selection_base = TextInput_erase(text_input.selection_base, text_input.selection_extent-1); + text_input.selection_extent = text_input.selection_base; + return true; +} +bool TextInput_addUtf8Char(char *c) { + size_t symbol_length; + char *to_move; + + if (text_input.selection_base != text_input.selection_extent) + TextInput_deleteSelected(); + + // 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); + + if (!to_move || !symbol_length) + return false; + + // move the string behind the insertion position to + // make place for the utf8 character + + memmove(to_move + symbol_length, to_move, strlen(to_move) + 1 /* null byte */); + + // after the move, to_move points to the memory + // where c should be inserted + for (int i = 0; i < symbol_length; i++) + to_move[i] = c[i]; + + // move our selection to behind the inserted char + text_input.selection_extent++; + text_input.selection_base = text_input.selection_extent; + + return true; +} +bool TextInput_backspace(void) { + if (text_input.selection_base != text_input.selection_extent) + return TextInput_deleteSelected(); + + if (text_input.selection_base != 0) { + int base = text_input.selection_base - 1; + text_input.selection_base = TextInput_erase(base, base); + text_input.selection_extent = text_input.selection_base; + return true; + } + + return false; +} +bool TextInput_delete(void) { + if (text_input.selection_base != text_input.selection_extent) + return TextInput_deleteSelected(); + + if (text_input.selection_base < strlen(text_input.text)) { + text_input.selection_base = TextInput_erase(text_input.selection_base, text_input.selection_base); + text_input.selection_extent = text_input.selection_base; + return true; + } + + return false; +} +bool TextInput_moveCursorToBeginning(void) { + if ((text_input.selection_base != 0) || (text_input.selection_extent != 0)) { + text_input.selection_base = 0; + text_input.selection_extent = 0; + return true; + } + + return false; +} +bool TextInput_moveCursorToEnd(void) { + int end = strlen(text_input.text); + + if (text_input.selection_base != end) { + text_input.selection_base = end; + text_input.selection_extent = end; + return true; + } + + return false; +} +bool TextInput_moveCursorForward(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)) { + text_input.selection_extent++; + text_input.selection_base++; + return true; + } + + return false; +} +bool TextInput_moveCursorBack(void) { + if (text_input.selection_base != text_input.selection_extent) { + text_input.selection_extent = text_input.selection_base; + return true; + } + + if (text_input.selection_base > 0) { + text_input.selection_base--; + text_input.selection_extent--; + return true; + } + + return false; +} + + +// these two functions automatically sync the editing state with flutter if +// a change ocurred, so you don't explicitly need to call TextInput_syncEditingState(). +// `c` doesn't need to be NULL-terminated, the length of the char will be calculated +// using the start byte. +int TextInput_onUtf8Char(char *c) { + if (text_input.transaction_id == -1) + return 0; + + if (TextInput_addUtf8Char(c)) + return TextInput_syncEditingState(); + + return 0; +} + +int TextInput_onKey(glfw_key key) { + bool needs_sync = false; + bool perform_action = false; + int ok; + + if (text_input.transaction_id == -1) + return 0; + + switch (key) { + case GLFW_KEY_LEFT: + needs_sync = TextInput_moveCursorBack(); + break; + case GLFW_KEY_RIGHT: + needs_sync = TextInput_moveCursorForward(); + break; + case GLFW_KEY_END: + needs_sync = TextInput_moveCursorToEnd(); + break; + case GLFW_KEY_HOME: + needs_sync = TextInput_moveCursorToBeginning(); + break; + case GLFW_KEY_BACKSPACE: + needs_sync = TextInput_backspace(); + break; + case GLFW_KEY_DELETE: + needs_sync = TextInput_delete(); + break; + case GLFW_KEY_ENTER: + if (text_input.input_type == kInputTypeMultiline) + needs_sync = TextInput_addUtf8Char("\n"); + + perform_action = true; + break; + default: + break; + } + + if (needs_sync) { + ok = TextInput_syncEditingState(); + if (ok != 0) return ok; + } + + if (perform_action) { + ok = TextInput_performAction(text_input.input_action); + if (ok != 0) return ok; + } + + return 0; +} + + +int TextInput_init(void) { + text_input.text[0] = '\0'; + text_input.warned_about_autocorrect = false; + + PluginRegistry_setReceiver(TEXT_INPUT_CHANNEL, kJSONMethodCall, TextInput_onReceive); + + printf("[text_input] init.\n"); + + return 0; +} + +int TextInput_deinit(void) { + printf("[text_input] deinit.\n"); + + return 0; +} \ No newline at end of file diff --git a/src/plugins/text_input.h b/src/plugins/text_input.h new file mode 100644 index 00000000..01395938 --- /dev/null +++ b/src/plugins/text_input.h @@ -0,0 +1,65 @@ +#ifndef _TEXT_INPUT_H +#define _TEXT_INPUT_H + +#include + +#define TEXT_INPUT_CHANNEL "flutter/textinput" + +#define TEXT_INPUT_MAX_CHARS 8192 + +enum text_input_type { + kInputTypeText, + kInputTypeMultiline, + kInputTypeNumber, + kInputTypePhone, + kInputTypeDatetime, + kInputTypeEmailAddress, + kInputTypeUrl, + kInputTypeVisiblePassword, +}; + +enum text_input_action { + kTextInputActionNone, + kTextInputActionUnspecified, + kTextInputActionDone, + kTextInputActionGo, + kTextInputActionSearch, + kTextInputActionSend, + kTextInputActionNext, + kTextInputActionPrevious, + kTextInputActionContinueAction, + kTextInputActionJoin, + kTextInputActionRoute, + kTextInputActionEmergencyCall, + kTextInputActionNewline +}; + +// while text input configuration has more values, we only care about these two. +struct text_input_configuration { + bool autocorrect; + enum text_input_action input_action; +}; + +int TextInput_syncEditingState(void); +int TextInput_performAction(enum text_input_action action); +int TextInput_onConnectionClosed(void); + +// TextInput model functions (updating the text editing state) +bool TextInput_deleteSelected(void); +bool TextInput_addUtf8Char(char *c); +bool TextInput_backspace(void); +bool TextInput_delete(void); +bool TextInput_moveCursorToBeginning(void); +bool TextInput_moveCursorToEnd(void); +bool TextInput_moveCursorForward(void); +bool TextInput_moveCursorBack(void); + +// parses the input string as linux terminal input and calls the TextInput model functions +// accordingly. +int TextInput_onUtf8Char(char *c); +int TextInput_onKey(glfw_key key); + +int TextInput_init(void); +int TextInput_deinit(void); + +#endif \ No newline at end of file