/* * Copyright © 2012 Openismus GmbH * Copyright © 2012 Intel Corporation * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), * to deal in the Software without restriction, including without limitation * the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice (including the next * paragraph) shall be included in all copies or substantial portions of the * Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ #include "config.h" #include #include #include #include #include #include #include #include #include #include #include "shared/helpers.h" #include "shared/xalloc.h" #include "window.h" #include "text-input-unstable-v1-client-protocol.h" struct text_entry { struct widget *widget; struct window *window; char *text; int active; uint32_t cursor; uint32_t anchor; struct { char *text; int32_t cursor; char *commit; PangoAttrList *attr_list; } preedit; struct { PangoAttrList *attr_list; int32_t cursor; } preedit_info; struct { int32_t cursor; int32_t anchor; uint32_t delete_index; uint32_t delete_length; bool invalid_delete; } pending_commit; struct zwp_text_input_v1 *text_input; PangoLayout *layout; struct { xkb_mod_mask_t shift_mask; } keysym; uint32_t serial; uint32_t reset_serial; uint32_t content_purpose; uint32_t click_to_show; char *preferred_language; bool button_pressed; }; struct editor { struct zwp_text_input_manager_v1 *text_input_manager; struct wl_data_source *selection; char *selected_text; struct display *display; struct window *window; struct widget *widget; struct text_entry *entry; struct text_entry *editor; struct text_entry *active_entry; }; static const char * utf8_end_char(const char *p) { while ((*p & 0xc0) == 0x80) p++; return p; } static const char * utf8_prev_char(const char *s, const char *p) { for (--p; p >= s; --p) { if ((*p & 0xc0) != 0x80) return p; } return NULL; } static const char * utf8_next_char(const char *p) { if (*p != 0) return utf8_end_char(++p); return NULL; } static void move_up(const char *p, uint32_t *cursor) { const char *posr, *posr_i; char text[16]; xkb_keysym_to_utf8(XKB_KEY_Return, text, sizeof(text)); posr = strstr(p, text); while (posr) { if (*cursor > (unsigned)(posr-p)) { posr_i = strstr(posr+1, text); if (!posr_i || !(*cursor > (unsigned)(posr_i-p))) { *cursor = posr-p; break; } posr = posr_i; } else { break; } } } static void move_down(const char *p, uint32_t *cursor) { const char *posr; char text[16]; xkb_keysym_to_utf8(XKB_KEY_Return, text, sizeof(text)); posr = strstr(p, text); while (posr) { if (*cursor <= (unsigned)(posr-p)) { *cursor = posr-p + 1; break; } posr = strstr(posr+1, text); } } static void text_entry_redraw_handler(struct widget *widget, void *data); static void text_entry_button_handler(struct widget *widget, struct input *input, uint32_t time, uint32_t button, enum wl_pointer_button_state state, void *data); static void text_entry_touch_handler(struct widget *widget, struct input *input, uint32_t serial, uint32_t time, int32_t id, float tx, float ty, void *data); static int text_entry_motion_handler(struct widget *widget, struct input *input, uint32_t time, float x, float y, void *data); static void text_entry_insert_at_cursor(struct text_entry *entry, const char *text, int32_t cursor, int32_t anchor); static void text_entry_set_preedit(struct text_entry *entry, const char *preedit_text, int preedit_cursor); static void text_entry_delete_text(struct text_entry *entry, uint32_t index, uint32_t length); static void text_entry_delete_selected_text(struct text_entry *entry); static void text_entry_reset_preedit(struct text_entry *entry); static void text_entry_commit_and_reset(struct text_entry *entry); static void text_entry_get_cursor_rectangle(struct text_entry *entry, struct rectangle *rectangle); static void text_entry_update(struct text_entry *entry); static void text_input_commit_string(void *data, struct zwp_text_input_v1 *text_input, uint32_t serial, const char *text) { struct text_entry *entry = data; if ((entry->serial - serial) > (entry->serial - entry->reset_serial)) { fprintf(stderr, "Ignore commit. Serial: %u, Current: %u, Reset: %u\n", serial, entry->serial, entry->reset_serial); return; } if (entry->pending_commit.invalid_delete) { fprintf(stderr, "Ignore commit. Invalid previous delete_surrounding event.\n"); memset(&entry->pending_commit, 0, sizeof entry->pending_commit); return; } text_entry_reset_preedit(entry); if (entry->pending_commit.delete_length) { text_entry_delete_text(entry, entry->pending_commit.delete_index, entry->pending_commit.delete_length); } else { text_entry_delete_selected_text(entry); } text_entry_insert_at_cursor(entry, text, entry->pending_commit.cursor, entry->pending_commit.anchor); memset(&entry->pending_commit, 0, sizeof entry->pending_commit); widget_schedule_redraw(entry->widget); } static void clear_pending_preedit(struct text_entry *entry) { memset(&entry->pending_commit, 0, sizeof entry->pending_commit); pango_attr_list_unref(entry->preedit_info.attr_list); entry->preedit_info.cursor = 0; entry->preedit_info.attr_list = NULL; memset(&entry->preedit_info, 0, sizeof entry->preedit_info); } static void text_input_preedit_string(void *data, struct zwp_text_input_v1 *text_input, uint32_t serial, const char *text, const char *commit) { struct text_entry *entry = data; if ((entry->serial - serial) > (entry->serial - entry->reset_serial)) { fprintf(stderr, "Ignore preedit_string. Serial: %u, Current: %u, Reset: %u\n", serial, entry->serial, entry->reset_serial); clear_pending_preedit(entry); return; } if (entry->pending_commit.invalid_delete) { fprintf(stderr, "Ignore preedit_string. Invalid previous delete_surrounding event.\n"); clear_pending_preedit(entry); return; } if (entry->pending_commit.delete_length) { text_entry_delete_text(entry, entry->pending_commit.delete_index, entry->pending_commit.delete_length); } else { text_entry_delete_selected_text(entry); } text_entry_set_preedit(entry, text, entry->preedit_info.cursor); entry->preedit.commit = strdup(commit); entry->preedit.attr_list = pango_attr_list_ref(entry->preedit_info.attr_list); clear_pending_preedit(entry); text_entry_update(entry); widget_schedule_redraw(entry->widget); } static void text_input_delete_surrounding_text(void *data, struct zwp_text_input_v1 *text_input, int32_t index, uint32_t length) { struct text_entry *entry = data; uint32_t text_length; entry->pending_commit.delete_index = entry->cursor + index; entry->pending_commit.delete_length = length; entry->pending_commit.invalid_delete = false; text_length = strlen(entry->text); if (entry->pending_commit.delete_index > text_length || length > text_length || entry->pending_commit.delete_index + length > text_length) { fprintf(stderr, "delete_surrounding_text: Invalid index: %d," \ "length %u'; cursor: %u text length: %u\n", index, length, entry->cursor, text_length); entry->pending_commit.invalid_delete = true; return; } } static void text_input_cursor_position(void *data, struct zwp_text_input_v1 *text_input, int32_t index, int32_t anchor) { struct text_entry *entry = data; entry->pending_commit.cursor = index; entry->pending_commit.anchor = anchor; } static void text_input_preedit_styling(void *data, struct zwp_text_input_v1 *text_input, uint32_t index, uint32_t length, uint32_t style) { struct text_entry *entry = data; PangoAttribute *attr1 = NULL; PangoAttribute *attr2 = NULL; if (!entry->preedit_info.attr_list) entry->preedit_info.attr_list = pango_attr_list_new(); switch (style) { case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_DEFAULT: case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_UNDERLINE: attr1 = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE); break; case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_INCORRECT: attr1 = pango_attr_underline_new(PANGO_UNDERLINE_ERROR); attr2 = pango_attr_underline_color_new(65535, 0, 0); break; case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_SELECTION: attr1 = pango_attr_background_new(0.3 * 65535, 0.3 * 65535, 65535); attr2 = pango_attr_foreground_new(65535, 65535, 65535); break; case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_HIGHLIGHT: case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_ACTIVE: attr1 = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE); attr2 = pango_attr_weight_new(PANGO_WEIGHT_BOLD); break; case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_INACTIVE: attr1 = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE); attr2 = pango_attr_foreground_new(0.3 * 65535, 0.3 * 65535, 0.3 * 65535); break; } if (attr1) { attr1->start_index = entry->cursor + index; attr1->end_index = entry->cursor + index + length; pango_attr_list_insert(entry->preedit_info.attr_list, attr1); } if (attr2) { attr2->start_index = entry->cursor + index; attr2->end_index = entry->cursor + index + length; pango_attr_list_insert(entry->preedit_info.attr_list, attr2); } } static void text_input_preedit_cursor(void *data, struct zwp_text_input_v1 *text_input, int32_t index) { struct text_entry *entry = data; entry->preedit_info.cursor = index; } static void text_input_modifiers_map(void *data, struct zwp_text_input_v1 *text_input, struct wl_array *map) { struct text_entry *entry = data; entry->keysym.shift_mask = keysym_modifiers_get_mask(map, "Shift"); } static void text_input_keysym(void *data, struct zwp_text_input_v1 *text_input, uint32_t serial, uint32_t time, uint32_t key, uint32_t state, uint32_t modifiers) { struct text_entry *entry = data; const char *new_char; if (key == XKB_KEY_Left || key == XKB_KEY_Right) { if (state != WL_KEYBOARD_KEY_STATE_RELEASED) return; if (key == XKB_KEY_Left) new_char = utf8_prev_char(entry->text, entry->text + entry->cursor); else new_char = utf8_next_char(entry->text + entry->cursor); if (new_char != NULL) { entry->cursor = new_char - entry->text; } if (!(modifiers & entry->keysym.shift_mask)) entry->anchor = entry->cursor; widget_schedule_redraw(entry->widget); return; } if (key == XKB_KEY_Up || key == XKB_KEY_Down) { if (state != WL_KEYBOARD_KEY_STATE_RELEASED) return; if (key == XKB_KEY_Up) move_up(entry->text, &entry->cursor); else move_down(entry->text, &entry->cursor); if (!(modifiers & entry->keysym.shift_mask)) entry->anchor = entry->cursor; widget_schedule_redraw(entry->widget); return; } if (key == XKB_KEY_BackSpace) { const char *start, *end; if (state != WL_KEYBOARD_KEY_STATE_RELEASED) return; text_entry_commit_and_reset(entry); start = utf8_prev_char(entry->text, entry->text + entry->cursor); if (start == NULL) return; end = utf8_next_char(start); text_entry_delete_text(entry, start - entry->text, end - start); return; } if (key == XKB_KEY_Tab || key == XKB_KEY_KP_Enter || key == XKB_KEY_Return) { char text[16]; if (state != WL_KEYBOARD_KEY_STATE_RELEASED) return; xkb_keysym_to_utf8(key, text, sizeof(text)); text_entry_insert_at_cursor(entry, text, 0, 0); return; } } static void text_input_enter(void *data, struct zwp_text_input_v1 *text_input, struct wl_surface *surface) { struct text_entry *entry = data; if (surface != window_get_wl_surface(entry->window)) return; entry->active++; text_entry_update(entry); entry->reset_serial = entry->serial; widget_schedule_redraw(entry->widget); } static void text_input_leave(void *data, struct zwp_text_input_v1 *text_input) { struct text_entry *entry = data; text_entry_commit_and_reset(entry); entry->active--; if (!entry->active) zwp_text_input_v1_hide_input_panel(text_input); widget_schedule_redraw(entry->widget); } static void text_input_input_panel_state(void *data, struct zwp_text_input_v1 *text_input, uint32_t state) { } static void text_input_language(void *data, struct zwp_text_input_v1 *text_input, uint32_t serial, const char *language) { fprintf(stderr, "input language is %s \n", language); } static void text_input_text_direction(void *data, struct zwp_text_input_v1 *text_input, uint32_t serial, uint32_t direction) { struct text_entry *entry = data; PangoContext *context = pango_layout_get_context(entry->layout); PangoDirection pango_direction; switch (direction) { case ZWP_TEXT_INPUT_V1_TEXT_DIRECTION_LTR: pango_direction = PANGO_DIRECTION_LTR; break; case ZWP_TEXT_INPUT_V1_TEXT_DIRECTION_RTL: pango_direction = PANGO_DIRECTION_RTL; break; case ZWP_TEXT_INPUT_V1_TEXT_DIRECTION_AUTO: default: pango_direction = PANGO_DIRECTION_NEUTRAL; } pango_context_set_base_dir(context, pango_direction); } static const struct zwp_text_input_v1_listener text_input_listener = { text_input_enter, text_input_leave, text_input_modifiers_map, text_input_input_panel_state, text_input_preedit_string, text_input_preedit_styling, text_input_preedit_cursor, text_input_commit_string, text_input_cursor_position, text_input_delete_surrounding_text, text_input_keysym, text_input_language, text_input_text_direction }; static void data_source_target(void *data, struct wl_data_source *source, const char *mime_type) { } static void data_source_send(void *data, struct wl_data_source *source, const char *mime_type, int32_t fd) { struct editor *editor = data; if (write(fd, editor->selected_text, strlen(editor->selected_text) + 1) < 0) fprintf(stderr, "write failed: %m\n"); close(fd); } static void data_source_cancelled(void *data, struct wl_data_source *source) { wl_data_source_destroy(source); } static const struct wl_data_source_listener data_source_listener = { data_source_target, data_source_send, data_source_cancelled }; static void paste_func(void *buffer, size_t len, int32_t x, int32_t y, void *data) { struct editor *editor = data; struct text_entry *entry = editor->active_entry; char *pasted_text; if (!entry) return; pasted_text = malloc(len + 1); strncpy(pasted_text, buffer, len); pasted_text[len] = '\0'; text_entry_insert_at_cursor(entry, pasted_text, 0, 0); free(pasted_text); } static void editor_copy_cut(struct editor *editor, struct input *input, bool cut) { struct text_entry *entry = editor->active_entry; if (!entry) return; if (entry->cursor != entry->anchor) { int start_index = MIN(entry->cursor, entry->anchor); int end_index = MAX(entry->cursor, entry->anchor); int len = end_index - start_index; editor->selected_text = realloc(editor->selected_text, len + 1); strncpy(editor->selected_text, &entry->text[start_index], len); editor->selected_text[len] = '\0'; if (cut) text_entry_delete_text(entry, start_index, len); editor->selection = display_create_data_source(editor->display); wl_data_source_offer(editor->selection, "text/plain;charset=utf-8"); wl_data_source_add_listener(editor->selection, &data_source_listener, editor); input_set_selection(input, editor->selection, display_get_serial(editor->display)); } } static void editor_paste(struct editor *editor, struct input *input) { input_receive_selection_data(input, "text/plain;charset=utf-8", paste_func, editor); } static void menu_func(void *data, struct input *input, int index) { struct window *window = data; struct editor *editor = window_get_user_data(window); fprintf(stderr, "picked entry %d\n", index); switch (index) { case 0: editor_copy_cut(editor, input, true); break; case 1: editor_copy_cut(editor, input, false); break; case 2: editor_paste(editor, input); break; } } static void show_menu(struct editor *editor, struct input *input, uint32_t time) { int32_t x, y; static const char *entries[] = { "Cut", "Copy", "Paste" }; input_get_position(input, &x, &y); window_show_menu(editor->display, input, time, editor->window, x + 10, y + 20, menu_func, entries, ARRAY_LENGTH(entries)); } static struct text_entry* text_entry_create(struct editor *editor, const char *text) { struct text_entry *entry; entry = xzalloc(sizeof *entry); entry->widget = widget_add_widget(editor->widget, entry); entry->window = editor->window; entry->text = strdup(text); entry->active = 0; entry->cursor = strlen(text); entry->anchor = entry->cursor; entry->text_input = zwp_text_input_manager_v1_create_text_input(editor->text_input_manager); zwp_text_input_v1_add_listener(entry->text_input, &text_input_listener, entry); widget_set_redraw_handler(entry->widget, text_entry_redraw_handler); widget_set_button_handler(entry->widget, text_entry_button_handler); widget_set_motion_handler(entry->widget, text_entry_motion_handler); widget_set_touch_down_handler(entry->widget, text_entry_touch_handler); return entry; } static void text_entry_destroy(struct text_entry *entry) { widget_destroy(entry->widget); zwp_text_input_v1_destroy(entry->text_input); g_clear_object(&entry->layout); free(entry->text); free(entry->preferred_language); free(entry); } static void redraw_handler(struct widget *widget, void *data) { struct editor *editor = data; cairo_surface_t *surface; struct rectangle allocation; cairo_t *cr; surface = window_get_surface(editor->window); widget_get_allocation(editor->widget, &allocation); cr = cairo_create(surface); cairo_rectangle(cr, allocation.x, allocation.y, allocation.width, allocation.height); cairo_clip(cr); cairo_translate(cr, allocation.x, allocation.y); /* Draw background */ cairo_push_group(cr); cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); cairo_set_source_rgba(cr, 1, 1, 1, 1); cairo_rectangle(cr, 0, 0, allocation.width, allocation.height); cairo_fill(cr); cairo_pop_group_to_source(cr); cairo_paint(cr); cairo_destroy(cr); cairo_surface_destroy(surface); } static void text_entry_allocate(struct text_entry *entry, int32_t x, int32_t y, int32_t width, int32_t height) { widget_set_allocation(entry->widget, x, y, width, height); } static void resize_handler(struct widget *widget, int32_t width, int32_t height, void *data) { struct editor *editor = data; struct rectangle allocation; widget_get_allocation(editor->widget, &allocation); text_entry_allocate(editor->entry, allocation.x + 20, allocation.y + 20, width - 40, height / 2 - 40); text_entry_allocate(editor->editor, allocation.x + 20, allocation.y + height / 2 + 20, width - 40, height / 2 - 40); } static void text_entry_activate(struct text_entry *entry, struct wl_seat *seat) { struct wl_surface *surface = window_get_wl_surface(entry->window); if (entry->click_to_show && entry->active) { zwp_text_input_v1_show_input_panel(entry->text_input); return; } if (!entry->click_to_show) zwp_text_input_v1_show_input_panel(entry->text_input); zwp_text_input_v1_activate(entry->text_input, seat, surface); } static void text_entry_deactivate(struct text_entry *entry, struct wl_seat *seat) { zwp_text_input_v1_deactivate(entry->text_input, seat); } static void text_entry_update_layout(struct text_entry *entry) { char *text; PangoAttrList *attr_list; assert(entry->cursor <= (strlen(entry->text) + (entry->preedit.text ? strlen(entry->preedit.text) : 0))); if (entry->preedit.text) { text = xmalloc(strlen(entry->text) + strlen(entry->preedit.text) + 1); strncpy(text, entry->text, entry->cursor); strcpy(text + entry->cursor, entry->preedit.text); strcpy(text + entry->cursor + strlen(entry->preedit.text), entry->text + entry->cursor); } else { text = strdup(entry->text); } if (entry->cursor != entry->anchor) { int start_index = MIN(entry->cursor, entry->anchor); int end_index = MAX(entry->cursor, entry->anchor); PangoAttribute *attr; attr_list = pango_attr_list_copy(entry->preedit.attr_list); if (!attr_list) attr_list = pango_attr_list_new(); attr = pango_attr_background_new(0.3 * 65535, 0.3 * 65535, 65535); attr->start_index = start_index; attr->end_index = end_index; pango_attr_list_insert(attr_list, attr); attr = pango_attr_foreground_new(65535, 65535, 65535); attr->start_index = start_index; attr->end_index = end_index; pango_attr_list_insert(attr_list, attr); } else { attr_list = pango_attr_list_ref(entry->preedit.attr_list); } if (entry->preedit.text && !entry->preedit.attr_list) { PangoAttribute *attr; if (!attr_list) attr_list = pango_attr_list_new(); attr = pango_attr_underline_new(PANGO_UNDERLINE_SINGLE); attr->start_index = entry->cursor; attr->end_index = entry->cursor + strlen(entry->preedit.text); pango_attr_list_insert(attr_list, attr); } if (entry->layout) { pango_layout_set_text(entry->layout, text, -1); pango_layout_set_attributes(entry->layout, attr_list); } free(text); pango_attr_list_unref(attr_list); } static void text_entry_update(struct text_entry *entry) { struct rectangle cursor_rectangle; zwp_text_input_v1_set_content_type(entry->text_input, ZWP_TEXT_INPUT_V1_CONTENT_HINT_NONE, entry->content_purpose); zwp_text_input_v1_set_surrounding_text(entry->text_input, entry->text, entry->cursor, entry->anchor); if (entry->preferred_language) zwp_text_input_v1_set_preferred_language(entry->text_input, entry->preferred_language); text_entry_get_cursor_rectangle(entry, &cursor_rectangle); zwp_text_input_v1_set_cursor_rectangle(entry->text_input, cursor_rectangle.x, cursor_rectangle.y, cursor_rectangle.width, cursor_rectangle.height); zwp_text_input_v1_commit_state(entry->text_input, ++entry->serial); } static void text_entry_insert_at_cursor(struct text_entry *entry, const char *text, int32_t cursor, int32_t anchor) { char *new_text = xmalloc(strlen(entry->text) + strlen(text) + 1); strncpy(new_text, entry->text, entry->cursor); strcpy(new_text + entry->cursor, text); strcpy(new_text + entry->cursor + strlen(text), entry->text + entry->cursor); free(entry->text); entry->text = new_text; if (anchor >= 0) entry->anchor = entry->cursor + strlen(text) + anchor; else entry->anchor = entry->cursor + 1 + anchor; if (cursor >= 0) entry->cursor += strlen(text) + cursor; else entry->cursor += 1 + cursor; text_entry_update_layout(entry); widget_schedule_redraw(entry->widget); text_entry_update(entry); } static void text_entry_reset_preedit(struct text_entry *entry) { entry->preedit.cursor = 0; free(entry->preedit.text); entry->preedit.text = NULL; free(entry->preedit.commit); entry->preedit.commit = NULL; pango_attr_list_unref(entry->preedit.attr_list); entry->preedit.attr_list = NULL; } static void text_entry_commit_and_reset(struct text_entry *entry) { char *commit = NULL; if (entry->preedit.commit) commit = strdup(entry->preedit.commit); text_entry_reset_preedit(entry); if (commit) { text_entry_insert_at_cursor(entry, commit, 0, 0); free(commit); } zwp_text_input_v1_reset(entry->text_input); text_entry_update(entry); entry->reset_serial = entry->serial; } static void text_entry_set_preedit(struct text_entry *entry, const char *preedit_text, int preedit_cursor) { text_entry_reset_preedit(entry); if (!preedit_text) return; entry->preedit.text = strdup(preedit_text); entry->preedit.cursor = preedit_cursor; text_entry_update_layout(entry); widget_schedule_redraw(entry->widget); } static uint32_t text_entry_try_invoke_preedit_action(struct text_entry *entry, int32_t x, int32_t y, uint32_t button, enum wl_pointer_button_state state) { int index, trailing; uint32_t cursor; const char *text; if (!entry->preedit.text) return 0; pango_layout_xy_to_index(entry->layout, x * PANGO_SCALE, y * PANGO_SCALE, &index, &trailing); text = pango_layout_get_text(entry->layout); cursor = g_utf8_offset_to_pointer(text + index, trailing) - text; if (cursor < entry->cursor || cursor > entry->cursor + strlen(entry->preedit.text)) { return 0; } if (state == WL_POINTER_BUTTON_STATE_RELEASED) zwp_text_input_v1_invoke_action(entry->text_input, button, cursor - entry->cursor); return 1; } static bool text_entry_has_preedit(struct text_entry *entry) { return entry->preedit.text && (strlen(entry->preedit.text) > 0); } static void text_entry_set_cursor_position(struct text_entry *entry, int32_t x, int32_t y, bool move_anchor) { int index, trailing; const char *text; uint32_t cursor; pango_layout_xy_to_index(entry->layout, x * PANGO_SCALE, y * PANGO_SCALE, &index, &trailing); text = pango_layout_get_text(entry->layout); cursor = g_utf8_offset_to_pointer(text + index, trailing) - text; if (move_anchor) entry->anchor = cursor; if (text_entry_has_preedit(entry)) { text_entry_commit_and_reset(entry); assert(!text_entry_has_preedit(entry)); } if (entry->cursor == cursor) return; entry->cursor = cursor; text_entry_update_layout(entry); widget_schedule_redraw(entry->widget); text_entry_update(entry); } static void text_entry_delete_text(struct text_entry *entry, uint32_t index, uint32_t length) { uint32_t l; assert(index <= strlen(entry->text)); assert(index + length <= strlen(entry->text)); assert(index + length >= length); l = strlen(entry->text + index + length); memmove(entry->text + index, entry->text + index + length, l + 1); if (entry->cursor > (index + length)) entry->cursor -= length; else if (entry->cursor > index) entry->cursor = index; entry->anchor = entry->cursor; text_entry_update_layout(entry); widget_schedule_redraw(entry->widget); text_entry_update(entry); } static void text_entry_delete_selected_text(struct text_entry *entry) { uint32_t start_index = entry->anchor < entry->cursor ? entry->anchor : entry->cursor; uint32_t end_index = entry->anchor < entry->cursor ? entry->cursor : entry->anchor; if (entry->anchor == entry->cursor) return; text_entry_delete_text(entry, start_index, end_index - start_index); entry->anchor = entry->cursor; } static void text_entry_get_cursor_rectangle(struct text_entry *entry, struct rectangle *rectangle) { struct rectangle allocation; PangoRectangle extents; PangoRectangle cursor_pos; widget_get_allocation(entry->widget, &allocation); if (entry->preedit.text && entry->preedit.cursor < 0) { rectangle->x = 0; rectangle->y = 0; rectangle->width = 0; rectangle->height = 0; return; } pango_layout_get_extents(entry->layout, &extents, NULL); pango_layout_get_cursor_pos(entry->layout, entry->cursor + entry->preedit.cursor, &cursor_pos, NULL); rectangle->x = allocation.x + (allocation.height / 2) + PANGO_PIXELS(cursor_pos.x); rectangle->y = allocation.y + 10 + PANGO_PIXELS(cursor_pos.y); rectangle->width = PANGO_PIXELS(cursor_pos.width); rectangle->height = PANGO_PIXELS(cursor_pos.height); } static void text_entry_draw_cursor(struct text_entry *entry, cairo_t *cr) { PangoRectangle extents; PangoRectangle cursor_pos; if (entry->preedit.text && entry->preedit.cursor < 0) return; pango_layout_get_extents(entry->layout, &extents, NULL); pango_layout_get_cursor_pos(entry->layout, entry->cursor + entry->preedit.cursor, &cursor_pos, NULL); cairo_set_line_width(cr, 1.0); cairo_move_to(cr, PANGO_PIXELS(cursor_pos.x), PANGO_PIXELS(cursor_pos.y)); cairo_line_to(cr, PANGO_PIXELS(cursor_pos.x), PANGO_PIXELS(cursor_pos.y) + PANGO_PIXELS(cursor_pos.height)); cairo_stroke(cr); } static int text_offset_left(struct rectangle *allocation) { return 10; } static int text_offset_top(struct rectangle *allocation) { return allocation->height / 2; } static void text_entry_redraw_handler(struct widget *widget, void *data) { struct text_entry *entry = data; cairo_surface_t *surface; struct rectangle allocation; cairo_t *cr; surface = window_get_surface(entry->window); widget_get_allocation(entry->widget, &allocation); cr = cairo_create(surface); cairo_rectangle(cr, allocation.x, allocation.y, allocation.width, allocation.height); cairo_clip(cr); cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); cairo_push_group(cr); cairo_translate(cr, allocation.x, allocation.y); cairo_set_source_rgba(cr, 1, 1, 1, 1); cairo_rectangle(cr, 0, 0, allocation.width, allocation.height); cairo_fill(cr); cairo_set_operator(cr, CAIRO_OPERATOR_OVER); if (entry->active) { cairo_rectangle(cr, 0, 0, allocation.width, allocation.height); cairo_set_line_width (cr, 3); cairo_set_source_rgba(cr, 0, 0, 1, 1.0); cairo_stroke(cr); } cairo_set_source_rgba(cr, 0, 0, 0, 1); cairo_translate(cr, text_offset_left(&allocation), text_offset_top(&allocation)); if (!entry->layout) entry->layout = pango_cairo_create_layout(cr); else pango_cairo_update_layout(cr, entry->layout); text_entry_update_layout(entry); pango_cairo_show_layout(cr, entry->layout); text_entry_draw_cursor(entry, cr); cairo_pop_group_to_source(cr); cairo_paint(cr); cairo_destroy(cr); cairo_surface_destroy(surface); } static int text_entry_motion_handler(struct widget *widget, struct input *input, uint32_t time, float x, float y, void *data) { struct text_entry *entry = data; struct rectangle allocation; int tx, ty; if (!entry->button_pressed) { return CURSOR_IBEAM; } widget_get_allocation(entry->widget, &allocation); tx = x - allocation.x - text_offset_left(&allocation); ty = y - allocation.y - text_offset_top(&allocation); text_entry_set_cursor_position(entry, tx, ty, false); return CURSOR_IBEAM; } static void text_entry_button_handler(struct widget *widget, struct input *input, uint32_t time, uint32_t button, enum wl_pointer_button_state state, void *data) { struct text_entry *entry = data; struct rectangle allocation; struct editor *editor; int32_t x, y; uint32_t result; widget_get_allocation(entry->widget, &allocation); input_get_position(input, &x, &y); x -= allocation.x + text_offset_left(&allocation); y -= allocation.y + text_offset_top(&allocation); editor = window_get_user_data(entry->window); switch (button) { case BTN_LEFT: entry->button_pressed = (state == WL_POINTER_BUTTON_STATE_PRESSED); if (state == WL_POINTER_BUTTON_STATE_PRESSED) input_grab(input, entry->widget, button); else input_ungrab(input); break; case BTN_RIGHT: if (state == WL_POINTER_BUTTON_STATE_PRESSED) show_menu(editor, input, time); break; } if (text_entry_has_preedit(entry)) { result = text_entry_try_invoke_preedit_action(entry, x, y, button, state); if (result) return; } if (state == WL_POINTER_BUTTON_STATE_PRESSED && button == BTN_LEFT) { struct wl_seat *seat = input_get_seat(input); text_entry_activate(entry, seat); editor->active_entry = entry; text_entry_set_cursor_position(entry, x, y, true); } } static void text_entry_touch_handler(struct widget *widget, struct input *input, uint32_t serial, uint32_t time, int32_t id, float tx, float ty, void *data) { struct text_entry *entry = data; struct wl_seat *seat = input_get_seat(input); struct rectangle allocation; struct editor *editor; int32_t x, y; widget_get_allocation(entry->widget, &allocation); x = tx - (allocation.x + text_offset_left(&allocation)); y = ty - (allocation.y + text_offset_top(&allocation)); editor = window_get_user_data(entry->window); text_entry_activate(entry, seat); editor->active_entry = entry; text_entry_set_cursor_position(entry, x, y, true); } static void editor_button_handler(struct widget *widget, struct input *input, uint32_t time, uint32_t button, enum wl_pointer_button_state state, void *data) { struct editor *editor = data; if (button != BTN_LEFT) { return; } if (state == WL_POINTER_BUTTON_STATE_PRESSED) { struct wl_seat *seat = input_get_seat(input); text_entry_deactivate(editor->entry, seat); text_entry_deactivate(editor->editor, seat); editor->active_entry = NULL; } } static void editor_touch_handler(struct widget *widget, struct input *input, uint32_t serial, uint32_t time, int32_t id, float tx, float ty, void *data) { struct editor *editor = data; struct wl_seat *seat = input_get_seat(input); text_entry_deactivate(editor->entry, seat); text_entry_deactivate(editor->editor, seat); editor->active_entry = NULL; } static void keyboard_focus_handler(struct window *window, struct input *device, void *data) { struct editor *editor = data; window_schedule_redraw(editor->window); } static int handle_bound_key(struct editor *editor, struct input *input, uint32_t sym, uint32_t time) { switch (sym) { case XKB_KEY_X: editor_copy_cut(editor, input, true); return 1; case XKB_KEY_C: editor_copy_cut(editor, input, false); return 1; case XKB_KEY_V: editor_paste(editor, input); return 1; default: return 0; } } static void key_handler(struct window *window, struct input *input, uint32_t time, uint32_t key, uint32_t sym, enum wl_keyboard_key_state state, void *data) { struct editor *editor = data; struct text_entry *entry; const char *new_char; char text[16]; uint32_t modifiers; if (!editor->active_entry) return; entry = editor->active_entry; if (state != WL_KEYBOARD_KEY_STATE_PRESSED) return; modifiers = input_get_modifiers(input); if ((modifiers & MOD_CONTROL_MASK) && (modifiers & MOD_SHIFT_MASK) && handle_bound_key(editor, input, sym, time)) return; switch (sym) { case XKB_KEY_BackSpace: text_entry_commit_and_reset(entry); new_char = utf8_prev_char(entry->text, entry->text + entry->cursor); if (new_char != NULL) text_entry_delete_text(entry, new_char - entry->text, (entry->text + entry->cursor) - new_char); break; case XKB_KEY_Delete: text_entry_commit_and_reset(entry); new_char = utf8_next_char(entry->text + entry->cursor); if (new_char != NULL) text_entry_delete_text(entry, entry->cursor, new_char - (entry->text + entry->cursor)); break; case XKB_KEY_Left: text_entry_commit_and_reset(entry); new_char = utf8_prev_char(entry->text, entry->text + entry->cursor); if (new_char != NULL) { entry->cursor = new_char - entry->text; if (!(input_get_modifiers(input) & MOD_SHIFT_MASK)) entry->anchor = entry->cursor; widget_schedule_redraw(entry->widget); } break; case XKB_KEY_Right: text_entry_commit_and_reset(entry); new_char = utf8_next_char(entry->text + entry->cursor); if (new_char != NULL) { entry->cursor = new_char - entry->text; if (!(input_get_modifiers(input) & MOD_SHIFT_MASK)) entry->anchor = entry->cursor; widget_schedule_redraw(entry->widget); } break; case XKB_KEY_Up: text_entry_commit_and_reset(entry); move_up(entry->text, &entry->cursor); if (!(input_get_modifiers(input) & MOD_SHIFT_MASK)) entry->anchor = entry->cursor; widget_schedule_redraw(entry->widget); break; case XKB_KEY_Down: text_entry_commit_and_reset(entry); move_down(entry->text, &entry->cursor); if (!(input_get_modifiers(input) & MOD_SHIFT_MASK)) entry->anchor = entry->cursor; widget_schedule_redraw(entry->widget); break; case XKB_KEY_Escape: break; default: if (xkb_keysym_to_utf8(sym, text, sizeof(text)) <= 0) break; text_entry_commit_and_reset(entry); text_entry_insert_at_cursor(entry, text, 0, 0); break; } widget_schedule_redraw(entry->widget); } static void global_handler(struct display *display, uint32_t name, const char *interface, uint32_t version, void *data) { struct editor *editor = data; if (!strcmp(interface, "zwp_text_input_manager_v1")) { editor->text_input_manager = display_bind(display, name, &zwp_text_input_manager_v1_interface, 1); } } int main(int argc, char *argv[]) { struct editor editor; int i; uint32_t click_to_show = 0; const char *preferred_language = NULL; for (i = 1; i < argc; i++) { if (strcmp("--click-to-show", argv[i]) == 0) click_to_show = 1; else if (strcmp("--preferred-language", argv[i]) == 0 && i + 1 < argc) { preferred_language = argv[i + 1]; i++; } else { printf("Usage: %s [OPTIONS]\n" " --click-to-show\n" " --preferred-language LANGUAGE\n", argv[0]); return 1; } } memset(&editor, 0, sizeof editor); #ifdef HAVE_PANGO g_type_init(); #endif editor.display = display_create(&argc, argv); if (editor.display == NULL) { fprintf(stderr, "failed to create display: %m\n"); return -1; } display_set_user_data(editor.display, &editor); display_set_global_handler(editor.display, global_handler); if (editor.text_input_manager == NULL) { fprintf(stderr, "No text input manager global\n"); return -1; } editor.window = window_create(editor.display); editor.widget = window_frame_create(editor.window, &editor); editor.entry = text_entry_create(&editor, "Entry"); editor.entry->click_to_show = click_to_show; if (preferred_language) editor.entry->preferred_language = strdup(preferred_language); editor.editor = text_entry_create(&editor, "Numeric"); editor.editor->content_purpose = ZWP_TEXT_INPUT_V1_CONTENT_PURPOSE_NUMBER; editor.editor->click_to_show = click_to_show; editor.selection = NULL; editor.selected_text = NULL; window_set_title(editor.window, "Text Editor"); window_set_key_handler(editor.window, key_handler); window_set_keyboard_focus_handler(editor.window, keyboard_focus_handler); window_set_user_data(editor.window, &editor); widget_set_redraw_handler(editor.widget, redraw_handler); widget_set_resize_handler(editor.widget, resize_handler); widget_set_button_handler(editor.widget, editor_button_handler); widget_set_touch_down_handler(editor.widget, editor_touch_handler); window_schedule_resize(editor.window, 500, 400); display_run(editor.display); if (editor.selected_text) free(editor.selected_text); if (editor.selection) wl_data_source_destroy(editor.selection); text_entry_destroy(editor.entry); text_entry_destroy(editor.editor); widget_destroy(editor.widget); window_destroy(editor.window); display_destroy(editor.display); return 0; }