diff --git a/include/input/ime.h b/include/input/ime.h new file mode 100644 index 0000000..0e13f38 --- /dev/null +++ b/include/input/ime.h @@ -0,0 +1,85 @@ +/* SPDX-License-Identifier: GPL-2.0-only */ + +#ifndef LABWC_IME_H +#define LABWC_IME_H + +#include +#include +#include "labwc.h" + +struct keyboard; + +/* + * The relay structure manages the relationship between text-inputs and + * input-method on a given seat. Multiple text-inputs may be bound to a relay, + * but at most one will be "active" (commucating with input-method) at a time. + * At most one input-method may be bound to the seat. When an input-method and + * an active text-input is present, the relay passes messages between them. + */ +struct input_method_relay { + struct seat *seat; + struct wl_list text_inputs; /* struct text_input.link */ + struct wlr_input_method_v2 *input_method; + struct wlr_surface *focused_surface; + /* + * Text-input which is enabled by the client and communicating with + * input-method. + * This must be NULL if input-method is not present. + * Its client must be the same as that of focused_surface. + */ + struct text_input *active_text_input; + + struct wlr_input_popup_surface_v2 *popup_surface; + struct wlr_scene_tree *popup_tree; + + struct wl_listener new_text_input; + struct wl_listener new_input_method; + + struct wl_listener input_method_commit; + struct wl_listener input_method_grab_keyboard; + struct wl_listener input_method_destroy; + struct wl_listener input_method_new_popup_surface; + + struct wl_listener popup_surface_destroy; + struct wl_listener popup_surface_commit; + + struct wl_listener keyboard_grab_destroy; + struct wl_listener focused_surface_destroy; +}; + +struct text_input { + struct input_method_relay *relay; + struct wlr_text_input_v3 *input; + bool v1; + struct wl_list link; + + struct wl_listener enable; + struct wl_listener commit; + struct wl_listener disable; + struct wl_listener destroy; +}; + +/* + * Forward key event to keyboard grab of the seat from the keyboard + * if the keyboard grab exists. + * Returns true if the key event was forwarded. + */ +bool input_method_keyboard_grab_forward_key(struct keyboard *keyboard, + struct wlr_keyboard_key_event *event); + +/* + * Forward modifier state to keyboard grab of the seat from the keyboard + * if the keyboard grab exists. + * Returns true if the modifier state was forwarded. + */ +bool input_method_keyboard_grab_forward_modifiers(struct keyboard *keyboard); + +struct input_method_relay *input_method_relay_create(struct seat *seat); + +void input_method_relay_finish(struct input_method_relay *relay); + +/* Updates currently focused surface. Surface must belong to the same seat. */ +void input_method_relay_set_focus(struct input_method_relay *relay, + struct wlr_surface *surface); + +#endif diff --git a/include/labwc.h b/include/labwc.h index 70cef94..abe32a6 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -40,10 +40,13 @@ #include #include #include +#include +#include #include #include "config/keybind.h" #include "config/rcxml.h" #include "input/cursor.h" +#include "input/ime.h" #include "regions.h" #include "session-lock.h" #if HAVE_NLS @@ -120,6 +123,8 @@ struct seat { /* if set, views cannot receive focus */ struct wlr_layer_surface_v1 *focused_layer; + struct input_method_relay *input_method_relay; + /** * pressed view/surface/node will usually be NULL and is only set on * button press while the mouse is over a view or surface, and reset @@ -324,6 +329,9 @@ struct server { struct wlr_tearing_control_manager_v1 *tearing_control; struct wl_listener tearing_new_object; + struct wlr_input_method_manager_v2 *input_method_manager; + struct wlr_text_input_manager_v3 *text_input_manager; + /* Set when in cycle (alt-tab) mode */ struct osd_state { struct view *cycle_view; diff --git a/include/node.h b/include/node.h index 419aa99..6d44664 100644 --- a/include/node.h +++ b/include/node.h @@ -15,6 +15,7 @@ enum node_descriptor_type { LAB_NODE_DESC_XDG_POPUP, LAB_NODE_DESC_LAYER_SURFACE, LAB_NODE_DESC_LAYER_POPUP, + LAB_NODE_DESC_INPUT_METHOD_POPUP, LAB_NODE_DESC_MENUITEM, LAB_NODE_DESC_TREE, LAB_NODE_DESC_SSD_BUTTON, diff --git a/protocols/meson.build b/protocols/meson.build index 0ec9154..60df3d4 100644 --- a/protocols/meson.build +++ b/protocols/meson.build @@ -19,6 +19,7 @@ server_protocols = [ wl_protocol_dir / 'staging/cursor-shape/cursor-shape-v1.xml', wl_protocol_dir / 'staging/drm-lease/drm-lease-v1.xml', wl_protocol_dir / 'staging/tearing-control/tearing-control-v1.xml', + wl_protocol_dir / 'unstable/text-input/text-input-unstable-v1.xml', 'wlr-layer-shell-unstable-v1.xml', 'wlr-input-inhibitor-unstable-v1.xml', 'wlr-output-power-management-unstable-v1.xml', diff --git a/src/desktop.c b/src/desktop.c index 35d56b8..aac7f73 100644 --- a/src/desktop.c +++ b/src/desktop.c @@ -416,6 +416,11 @@ get_cursor_context(struct server *server) ret.type = LAB_SSD_CLIENT; ret.surface = get_surface_from_layer_node(node); return ret; + case LAB_NODE_DESC_INPUT_METHOD_POPUP: + ret.node = node; + ret.type = LAB_SSD_CLIENT; + ret.surface = lab_wlr_surface_from_node(node); + return ret; case LAB_NODE_DESC_MENUITEM: /* Always return the top scene node for menu items */ ret.node = node; diff --git a/src/input/ime.c b/src/input/ime.c new file mode 100644 index 0000000..4e35400 --- /dev/null +++ b/src/input/ime.c @@ -0,0 +1,917 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Based on Sway (https://github.com/swaywm/sway) */ + +#include +#include "common/mem.h" +#include "input/ime.h" +#include "node.h" +#include "view.h" +#include "text-input-unstable-v1-protocol.h" + +#define SAME_CLIENT(wlr_obj1, wlr_obj2) \ + (wl_resource_get_client((wlr_obj1)->resource) \ + == wl_resource_get_client((wlr_obj2)->resource)) + +/* ---------------------------------------------------------------------------- + * text-input-unstable-v1 protocol support + * ---------------------------------------------------------------------------- + * To reuse code for text-input-unstable-v3, zwp_text_input_v1 interface is + * wrapped as wlr_text_input_v3, by converting zwp_text_input_v1-specific + * events and emitting them as wlr_text_input_v3's events. When + * zwp_text_input_manager_v1.create_text_input event is received, a wrapped + * wlr_text_input_v3 is created and added to + * wlr_text_input_manager_v3.text_inputs. + * ---------------------------------------------------------------------------- + */ + +struct text_input_manager_v1 { + struct wl_global *global; + struct wlr_text_input_manager_v3 *wlr_manager_v3; + struct wl_listener display_destroy; +}; + +static void +text_input_v1_clear_focused_surface(struct wlr_text_input_v3 *text_input) +{ + if (text_input->focused_surface) { + wl_list_remove(&text_input->surface_destroy.link); + } + text_input->focused_surface = NULL; +} + +static void +text_input_v1_clear_seat(struct wlr_text_input_v3 *text_input) +{ + if (text_input->seat) { + wl_list_remove(&text_input->seat_destroy.link); + } + text_input->seat = NULL; +} + +static void +text_input_v1_destroy(struct wlr_text_input_v3 *text_input) +{ + wl_signal_emit_mutable(&text_input->events.destroy, NULL); + + text_input_v1_clear_focused_surface(text_input); + text_input_v1_clear_seat(text_input); + + wl_list_remove(&text_input->link); + free(text_input->current.surrounding.text); + free(text_input); +} + +static void +handle_text_input_v1_focused_surface_destroy(struct wl_listener *listener, + void *data) +{ + struct wlr_text_input_v3 *text_input = + wl_container_of(listener, text_input, surface_destroy); + text_input_v1_clear_focused_surface(text_input); +} + +static void +handle_text_input_v1_activate(struct wl_client *client, + struct wl_resource *resource, struct wl_resource *seat, + struct wl_resource *surface) +{ + struct wlr_text_input_v3 *text_input = wl_resource_get_user_data(resource); + struct wlr_seat_client *seat_client = wlr_seat_client_from_resource(seat); + + text_input->seat = seat_client->seat; + wl_signal_add(&seat_client->events.destroy, &text_input->seat_destroy); + + text_input->current_enabled = true; + text_input->active_features = text_input->current.features; + wl_signal_emit_mutable(&text_input->events.enable, NULL); +} + +static void +handle_text_input_v1_deactivate(struct wl_client *client, + struct wl_resource *resource, struct wl_resource *seat) +{ + struct wlr_text_input_v3 *text_input = wl_resource_get_user_data(resource); + + text_input_v1_clear_seat(text_input); + + text_input->current_enabled = false; + text_input->active_features = 0; + wl_signal_emit_mutable(&text_input->events.disable, NULL); +} + +static void +handle_text_input_v1_reset(struct wl_client *client, struct wl_resource *resource) +{ + struct wlr_text_input_v3 *text_input = wl_resource_get_user_data(resource); + text_input->current = (struct wlr_text_input_v3_state){0}; + free(text_input->current.surrounding.text); + wl_signal_emit_mutable(&text_input->events.commit, NULL); +} + +static void +handle_text_input_v1_set_surrounding_text(struct wl_client *client, + struct wl_resource *resource, const char *text, + uint32_t cursor, uint32_t anchor) +{ + struct wlr_text_input_v3 *text_input = wl_resource_get_user_data(resource); + + free(text_input->current.surrounding.text); + text_input->current.surrounding.text = xstrdup(text); + text_input->current.surrounding.cursor = cursor; + text_input->current.surrounding.anchor = anchor; + text_input->current.features |= WLR_TEXT_INPUT_V3_FEATURE_SURROUNDING_TEXT; + text_input->active_features = text_input->current.features; + wl_signal_emit_mutable(&text_input->events.commit, NULL); +} + +static void +handle_text_input_v1_set_cursor_rectangle(struct wl_client *client, + struct wl_resource *resource, int32_t x, int32_t y, + int32_t width, int32_t height) +{ + struct wlr_text_input_v3 *text_input = wl_resource_get_user_data(resource); + text_input->current.cursor_rectangle.x = x; + text_input->current.cursor_rectangle.y = y; + text_input->current.cursor_rectangle.width = width; + text_input->current.cursor_rectangle.height = height; + text_input->current.features |= WLR_TEXT_INPUT_V3_FEATURE_CURSOR_RECTANGLE; + text_input->active_features = text_input->current.features; + wl_signal_emit_mutable(&text_input->events.commit, NULL); +} + +static void +handle_text_input_v1_commit_state(struct wl_client *client, + struct wl_resource *resource, uint32_t serial) +{ + struct wlr_text_input_v3 *text_input = wl_resource_get_user_data(resource); + text_input->current_serial = serial; +} + +static void noop(void){} +static const struct zwp_text_input_v1_interface text_input_impl = { + .activate = handle_text_input_v1_activate, + .deactivate = handle_text_input_v1_deactivate, + .show_input_panel = (__typeof__(text_input_impl.show_input_panel))noop, + .hide_input_panel = (__typeof__(text_input_impl.hide_input_panel))noop, + .reset = handle_text_input_v1_reset, + .set_surrounding_text = handle_text_input_v1_set_surrounding_text, + .set_content_type = (__typeof__(text_input_impl.set_content_type))noop, + .set_cursor_rectangle = handle_text_input_v1_set_cursor_rectangle, + .set_preferred_language = (__typeof__(text_input_impl.set_preferred_language))noop, + .commit_state = handle_text_input_v1_commit_state, + .invoke_action = (__typeof__(text_input_impl.invoke_action))noop, +}; + +static void +handle_text_input_v1_resource_destroy(struct wl_resource *resource) +{ + struct wlr_text_input_v3 *text_input = wl_resource_get_user_data(resource); + if (!text_input) { + return; + } + text_input_v1_destroy(text_input); + wl_resource_set_user_data(resource, NULL); +} + +static void +handle_text_input_v1_seat_destroy(struct wl_listener *listener, void *data) +{ + struct wlr_text_input_v3 *text_input = + wl_container_of(listener, text_input, seat_destroy); + if (!text_input) { + return; + } + struct wl_resource *resource = text_input->resource; + text_input_v1_destroy(text_input); + wl_resource_set_user_data(resource, NULL); +} + +static void +handle_manager_v1_create_text_input(struct wl_client *client, + struct wl_resource *resource, uint32_t id) +{ + int version = wl_resource_get_version(resource); + struct wl_resource *text_input_resource = wl_resource_create(client, + &zwp_text_input_v1_interface, version, id); + struct text_input_manager_v1 *manager_v1 = wl_resource_get_user_data(resource); + struct wlr_text_input_manager_v3 *wlr_manager_v3 = manager_v1->wlr_manager_v3; + + struct wlr_text_input_v3 *text_input = znew(*text_input); + text_input->resource = text_input_resource; + + wl_signal_init(&text_input->events.commit); + wl_signal_init(&text_input->events.enable); + wl_signal_init(&text_input->events.disable); + wl_signal_init(&text_input->events.destroy); + + wl_resource_set_implementation(text_input_resource, &text_input_impl, + text_input, handle_text_input_v1_resource_destroy); + + text_input->seat_destroy.notify = + handle_text_input_v1_seat_destroy; + text_input->surface_destroy.notify = + handle_text_input_v1_focused_surface_destroy; + + wl_list_insert(&wlr_manager_v3->text_inputs, &text_input->link); + + wl_signal_emit_mutable(&wlr_manager_v3->events.text_input, text_input); +} + +static const struct zwp_text_input_manager_v1_interface text_input_manager_v1_impl = { + .create_text_input = handle_manager_v1_create_text_input, +}; + +static void +handle_manager_v1_bind(struct wl_client *wl_client, void *data, + uint32_t version, uint32_t id) +{ + struct text_input_manager_v1 *manager_v1 = data; + + struct wl_resource *resource = wl_resource_create(wl_client, + &zwp_text_input_manager_v1_interface, version, id); + wl_resource_set_implementation(resource, &text_input_manager_v1_impl, + manager_v1, NULL); +} + +static void handle_display_destroy(struct wl_listener *listener, void *data) +{ + struct text_input_manager_v1 *manager_v1 = + wl_container_of(listener, manager_v1, display_destroy); + wl_list_remove(&manager_v1->display_destroy.link); + wl_global_destroy(manager_v1->global); + free(manager_v1); +} + +static void +text_input_manager_v1_init(struct wl_display *display, + struct wlr_text_input_manager_v3 *wlr_manager_v3) +{ + struct text_input_manager_v1 *manager_v1 = znew(*manager_v1); + manager_v1->wlr_manager_v3 = wlr_manager_v3; + manager_v1->global = wl_global_create(display, + &zwp_text_input_manager_v1_interface, 1, manager_v1, + handle_manager_v1_bind); + manager_v1->display_destroy.notify = handle_display_destroy; + wl_display_add_destroy_listener(display, &manager_v1->display_destroy); +} + +static void +text_input_v1_send_enter(struct wlr_text_input_v3 *text_input, + struct wlr_surface *surface) +{ + assert(!text_input->focused_surface); + text_input->focused_surface = surface; + wl_signal_add(&text_input->focused_surface->events.destroy, + &text_input->surface_destroy); + zwp_text_input_v1_send_enter(text_input->resource, + surface->resource); +} + +static void +text_input_v1_send_leave(struct wlr_text_input_v3 *text_input) +{ + assert(text_input->focused_surface); + text_input_v1_clear_focused_surface(text_input); + zwp_text_input_v1_send_leave(text_input->resource); +} + +/* + * Get keyboard grab of the seat from keyboard if we should forward events + * to it. + */ +static struct wlr_input_method_keyboard_grab_v2 * +get_keyboard_grab(struct keyboard *keyboard) +{ + struct wlr_input_method_v2 *input_method = + keyboard->base.seat->input_method_relay->input_method; + if (!input_method || !input_method->keyboard_grab) { + return NULL; + } + + /* + * Input-methods often use virtual keyboard to send raw key signals + * instead of sending encoded text via set_preedit_string and + * commit_string. We should not forward those key events back to the + * input-method so key events don't loop between the compositor and + * the input-method. + */ + struct wlr_virtual_keyboard_v1 *virtual_keyboard = + wlr_input_device_get_virtual_keyboard( + keyboard->base.wlr_input_device); + if (virtual_keyboard && SAME_CLIENT(virtual_keyboard, + input_method->keyboard_grab)) { + return NULL; + } + + return input_method->keyboard_grab; +} + +bool +input_method_keyboard_grab_forward_modifiers(struct keyboard *keyboard) +{ + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = + get_keyboard_grab(keyboard); + if (keyboard_grab) { + wlr_input_method_keyboard_grab_v2_set_keyboard(keyboard_grab, + keyboard->wlr_keyboard); + wlr_input_method_keyboard_grab_v2_send_modifiers(keyboard_grab, + &keyboard->wlr_keyboard->modifiers); + return true; + } else { + return false; + } +} + +bool +input_method_keyboard_grab_forward_key(struct keyboard *keyboard, + struct wlr_keyboard_key_event *event) +{ + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = + get_keyboard_grab(keyboard); + if (keyboard_grab) { + wlr_input_method_keyboard_grab_v2_set_keyboard(keyboard_grab, + keyboard->wlr_keyboard); + wlr_input_method_keyboard_grab_v2_send_key(keyboard_grab, + event->time_msec, event->keycode, event->state); + return true; + } else { + return false; + } +} + +/* + * update_text_inputs_focused_surface() should be called beforehand to set + * right text-inputs to choose from. + */ +static struct text_input * +get_active_text_input(struct input_method_relay *relay) +{ + if (!relay->input_method) { + return NULL; + } + struct text_input *text_input; + wl_list_for_each(text_input, &relay->text_inputs, link) { + if (text_input->input->focused_surface + && text_input->input->current_enabled) { + return text_input; + } + } + return NULL; +} + +/* + * Updates active text-input and activates/deactivates the input-method if the + * value is changed. + */ +static void +update_active_text_input(struct input_method_relay *relay) +{ + struct text_input *active_text_input = get_active_text_input(relay); + + if (relay->input_method && relay->active_text_input + != active_text_input) { + if (active_text_input) { + wlr_input_method_v2_send_activate(relay->input_method); + } else { + wlr_input_method_v2_send_deactivate(relay->input_method); + } + wlr_input_method_v2_send_done(relay->input_method); + } + + relay->active_text_input = active_text_input; +} + +/* + * Updates focused surface of text-inputs and sends enter/leave events to + * the text-inputs whose focused surface is changed. + * When input-method is present, text-inputs whose client is the same as the + * relay's focused surface also have that focused surface. Clients can then + * send enable request on a text-input which has the focused surface to make + * the text-input active and start communicating with input-method. + */ +static void +update_text_inputs_focused_surface(struct input_method_relay *relay) +{ + struct text_input *text_input; + wl_list_for_each(text_input, &relay->text_inputs, link) { + struct wlr_text_input_v3 *input = text_input->input; + + struct wlr_surface *new_focused_surface; + if (relay->input_method && relay->focused_surface + && SAME_CLIENT(input, relay->focused_surface)) { + new_focused_surface = relay->focused_surface; + } else { + new_focused_surface = NULL; + } + + if (input->focused_surface == new_focused_surface) { + continue; + } + if (input->focused_surface) { + if (text_input->v1) + text_input_v1_send_leave(input); + else + wlr_text_input_v3_send_leave(input); + } + if (new_focused_surface) { + if (text_input->v1) + text_input_v1_send_enter(input, + relay->focused_surface); + else + wlr_text_input_v3_send_enter(input, new_focused_surface); + } + } +} + +static void +update_popup_position(struct input_method_relay *relay) +{ + struct server *server = relay->seat->server; + struct text_input *text_input = relay->active_text_input; + + if (!text_input || !relay->focused_surface || !relay->popup_surface + || !relay->popup_surface->surface->mapped) { + return; + } + + struct wlr_box cursor_rect; + struct wlr_xdg_surface *xdg_surface = + wlr_xdg_surface_try_from_wlr_surface(relay->focused_surface); + struct wlr_layer_surface_v1 *layer_surface = + wlr_layer_surface_v1_try_from_wlr_surface(relay->focused_surface); + + if ((text_input->input->current.features + & WLR_TEXT_INPUT_V3_FEATURE_CURSOR_RECTANGLE) + && (xdg_surface || layer_surface)) { + cursor_rect = text_input->input->current.cursor_rectangle; + + struct wlr_scene_tree *tree = relay->focused_surface->data; + int lx, ly; + wlr_scene_node_coords(&tree->node, &lx, &ly); + cursor_rect.x += lx; + cursor_rect.y += ly; + + if (xdg_surface) { + /* + * The position of cursor rectangle set by client + * is in wl_surface-coordinate so we need to convert + * it to offset relative to top-left corner of the + * view (set by xdg_surface.set_window_geometry). + */ + cursor_rect.x -= xdg_surface->current.geometry.x; + cursor_rect.y -= xdg_surface->current.geometry.y; + } + } else { + cursor_rect = (struct wlr_box){0}; + } + + struct output *output = + output_nearest_to(server, cursor_rect.x, cursor_rect.y); + if (!output_is_usable(output)) { + wlr_log(WLR_ERROR, + "Cannot position IME popup (invalid output)"); + return; + } + struct wlr_box output_box; + wlr_output_layout_get_box(server->output_layout, output->wlr_output, + &output_box); + + /* Use xdg-positioner utilities to position popup */ + struct wlr_xdg_positioner_rules rules = { + .anchor_rect = cursor_rect, + .anchor = XDG_POSITIONER_ANCHOR_BOTTOM_LEFT, + .gravity = XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT, + .size = { + .width = relay->popup_surface->surface->current.width, + .height = relay->popup_surface->surface->current.height, + }, + .constraint_adjustment = + XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_FLIP_Y + | XDG_POSITIONER_CONSTRAINT_ADJUSTMENT_SLIDE_X, + }; + + /* Calculate the popup position */ + struct wlr_box popup_box; + wlr_xdg_positioner_rules_get_geometry(&rules, &popup_box); + wlr_xdg_positioner_rules_unconstrain_box(&rules, &output_box, &popup_box); + + wlr_scene_node_set_position(&relay->popup_tree->node, + popup_box.x, popup_box.y); + + wlr_input_popup_surface_v2_send_text_input_rectangle( + relay->popup_surface, &(struct wlr_box){ + .x = cursor_rect.x - popup_box.x, + .y = cursor_rect.y - popup_box.y, + .width = cursor_rect.width, + .height = cursor_rect.height, + }); +} + +static void +handle_input_method_commit(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, input_method_commit); + struct wlr_input_method_v2 *input_method = data; + assert(relay->input_method == input_method); + + struct text_input *text_input = relay->active_text_input; + if (!text_input) { + return; + } + + if (text_input->v1) { + /* + * For some reason, if I send commit_string after preedit_string, + * the text is doubled in the integrated terminal of VSCode. + */ + if (input_method->current.commit_text) { + zwp_text_input_v1_send_commit_string( + text_input->input->resource, + text_input->input->current_serial, + input_method->current.commit_text); + } + if (input_method->current.preedit.text) { + int preedit_len = strlen(input_method->current.preedit.text); + zwp_text_input_v1_send_preedit_cursor( + text_input->input->resource, + input_method->current.preedit.cursor_begin + ); + if (0 < input_method->current.preedit.cursor_begin) { + zwp_text_input_v1_send_preedit_styling( + text_input->input->resource, 0, + input_method->current.preedit.cursor_begin, + ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_UNDERLINE); + } + if (input_method->current.preedit.cursor_begin + < input_method->current.preedit.cursor_end) { + zwp_text_input_v1_send_preedit_styling( + text_input->input->resource, + input_method->current.preedit.cursor_begin, + input_method->current.preedit.cursor_end + - input_method->current.preedit.cursor_begin, + ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_HIGHLIGHT); + } + if (input_method->current.preedit.cursor_end < preedit_len) { + zwp_text_input_v1_send_preedit_styling( + text_input->input->resource, + input_method->current.preedit.cursor_end, + preedit_len - input_method->current.preedit.cursor_end, + ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_UNDERLINE); + } + zwp_text_input_v1_send_preedit_string( + text_input->input->resource, + text_input->input->current_serial, + input_method->current.preedit.text, + input_method->current.preedit.text); + } else { + zwp_text_input_v1_send_preedit_cursor( + text_input->input->resource, 0); + zwp_text_input_v1_send_preedit_styling( + text_input->input->resource, 0, 0, + ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_NONE); + zwp_text_input_v1_send_preedit_string( + text_input->input->resource, + text_input->input->current_serial, "", ""); + } + return; + } + if (input_method->current.preedit.text) { + wlr_text_input_v3_send_preedit_string( + text_input->input, + input_method->current.preedit.text, + input_method->current.preedit.cursor_begin, + input_method->current.preedit.cursor_end); + } + if (input_method->current.commit_text) { + wlr_text_input_v3_send_commit_string( + text_input->input, + input_method->current.commit_text); + } + if (input_method->current.delete.before_length + || input_method->current.delete.after_length) { + wlr_text_input_v3_send_delete_surrounding_text( + text_input->input, + input_method->current.delete.before_length, + input_method->current.delete.after_length); + } + wlr_text_input_v3_send_done(text_input->input); +} + +static void +handle_keyboard_grab_destroy(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, keyboard_grab_destroy); + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = data; + wl_list_remove(&relay->keyboard_grab_destroy.link); + + if (keyboard_grab->keyboard) { + /* Send modifier state to original client */ + wlr_seat_keyboard_notify_modifiers( + keyboard_grab->input_method->seat, + &keyboard_grab->keyboard->modifiers); + } +} + +static void +handle_input_method_grab_keyboard(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = wl_container_of(listener, relay, + input_method_grab_keyboard); + struct wlr_input_method_keyboard_grab_v2 *keyboard_grab = data; + + /* Send modifier state to grab */ + struct wlr_keyboard *active_keyboard = + wlr_seat_get_keyboard(relay->seat->seat); + wlr_input_method_keyboard_grab_v2_set_keyboard( + keyboard_grab, active_keyboard); + + relay->keyboard_grab_destroy.notify = handle_keyboard_grab_destroy; + wl_signal_add(&keyboard_grab->events.destroy, + &relay->keyboard_grab_destroy); +} + +static void +handle_input_method_destroy(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, input_method_destroy); + assert(relay->input_method == data); + wl_list_remove(&relay->input_method_commit.link); + wl_list_remove(&relay->input_method_grab_keyboard.link); + wl_list_remove(&relay->input_method_destroy.link); + relay->input_method = NULL; + + update_text_inputs_focused_surface(relay); + update_active_text_input(relay); +} + +static void +handle_popup_surface_destroy(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = wl_container_of(listener, relay, + popup_surface_destroy); + wl_list_remove(&relay->popup_surface_destroy.link); + wl_list_remove(&relay->popup_surface_commit.link); + relay->popup_surface = NULL; +} + +static void +handle_popup_surface_commit(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = wl_container_of(listener, relay, + popup_surface_commit); + update_popup_position(relay); +} + +static void +handle_input_method_new_popup_surface(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = wl_container_of(listener, relay, + input_method_new_popup_surface); + + if (relay->popup_surface) { + wlr_log(WLR_INFO, "Multiple IME popups are not supported"); + return; + } + + relay->popup_surface = data; + + wl_signal_add(&relay->popup_surface->events.destroy, + &relay->popup_surface_destroy); + wl_signal_add(&relay->popup_surface->surface->events.commit, + &relay->popup_surface_commit); + + struct wlr_scene_node *scene_node = &wlr_scene_surface_create( + relay->popup_tree, relay->popup_surface->surface)->buffer->node; + node_descriptor_create(scene_node, LAB_NODE_DESC_INPUT_METHOD_POPUP, NULL); + + update_popup_position(relay); +} + +static void +handle_new_input_method(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, new_input_method); + struct wlr_input_method_v2 *input_method = data; + if (relay->seat->seat != input_method->seat) { + return; + } + + if (relay->input_method) { + wlr_log(WLR_INFO, + "Attempted to connect second input method to a seat"); + wlr_input_method_v2_send_unavailable(input_method); + return; + } + + relay->input_method = input_method; + + relay->input_method_commit.notify = handle_input_method_commit; + wl_signal_add(&relay->input_method->events.commit, + &relay->input_method_commit); + + relay->input_method_grab_keyboard.notify = + handle_input_method_grab_keyboard; + wl_signal_add(&relay->input_method->events.grab_keyboard, + &relay->input_method_grab_keyboard); + + relay->input_method_destroy.notify = handle_input_method_destroy; + wl_signal_add(&relay->input_method->events.destroy, + &relay->input_method_destroy); + + relay->input_method_new_popup_surface.notify = + handle_input_method_new_popup_surface; + wl_signal_add(&relay->input_method->events.new_popup_surface, + &relay->input_method_new_popup_surface); + + update_text_inputs_focused_surface(relay); + update_active_text_input(relay); +} + +/* Conveys state from active text-input to input-method */ +static void +send_state_to_input_method(struct input_method_relay *relay) +{ + assert(relay->active_text_input && relay->input_method); + + struct wlr_input_method_v2 *input_method = relay->input_method; + struct wlr_text_input_v3 *input = relay->active_text_input->input; + + /* TODO: only send each of those if they were modified */ + if (input->active_features + & WLR_TEXT_INPUT_V3_FEATURE_SURROUNDING_TEXT) { + wlr_input_method_v2_send_surrounding_text(input_method, + input->current.surrounding.text, + input->current.surrounding.cursor, + input->current.surrounding.anchor); + } + wlr_input_method_v2_send_text_change_cause(input_method, + input->current.text_change_cause); + if (input->active_features & WLR_TEXT_INPUT_V3_FEATURE_CONTENT_TYPE) { + wlr_input_method_v2_send_content_type(input_method, + input->current.content_type.hint, + input->current.content_type.purpose); + } + wlr_input_method_v2_send_done(input_method); +} + +static void +handle_text_input_enable(struct wl_listener *listener, void *data) +{ + struct text_input *text_input = + wl_container_of(listener, text_input, enable); + struct input_method_relay *relay = text_input->relay; + + update_active_text_input(relay); + if (relay->active_text_input == text_input) { + send_state_to_input_method(relay); + } +} + +static void +handle_text_input_disable(struct wl_listener *listener, void *data) +{ + struct text_input *text_input = + wl_container_of(listener, text_input, disable); + /* + * When the focus is moved between surfaces from different clients and + * then the old client sends "disable" event, the relay ignores it and + * doesn't deactivate the input-method. + */ + update_active_text_input(text_input->relay); +} + +static void +handle_text_input_commit(struct wl_listener *listener, void *data) +{ + struct text_input *text_input = + wl_container_of(listener, text_input, commit); + struct input_method_relay *relay = text_input->relay; + + if (relay->active_text_input == text_input) { + update_popup_position(relay); + send_state_to_input_method(relay); + } +} + +static void +handle_text_input_destroy(struct wl_listener *listener, void *data) +{ + struct text_input *text_input = + wl_container_of(listener, text_input, destroy); + wl_list_remove(&text_input->enable.link); + wl_list_remove(&text_input->disable.link); + wl_list_remove(&text_input->commit.link); + wl_list_remove(&text_input->destroy.link); + wl_list_remove(&text_input->link); + update_active_text_input(text_input->relay); + free(text_input); +} + +static void +handle_new_text_input(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, new_text_input); + struct wlr_text_input_v3 *wlr_text_input = data; + bool is_v1 = !strcmp("zwp_text_input_v1", + wl_resource_get_class(wlr_text_input->resource)); + if (!is_v1 && relay->seat->seat != wlr_text_input->seat) { + return; + } + + struct text_input *text_input = znew(*text_input); + text_input->v1 = is_v1; + text_input->input = wlr_text_input; + text_input->relay = relay; + wl_list_insert(&relay->text_inputs, &text_input->link); + + text_input->enable.notify = handle_text_input_enable; + wl_signal_add(&text_input->input->events.enable, &text_input->enable); + + text_input->disable.notify = handle_text_input_disable; + wl_signal_add(&text_input->input->events.disable, &text_input->disable); + + text_input->commit.notify = handle_text_input_commit; + wl_signal_add(&text_input->input->events.commit, &text_input->commit); + + text_input->destroy.notify = handle_text_input_destroy; + wl_signal_add(&text_input->input->events.destroy, &text_input->destroy); + + update_text_inputs_focused_surface(relay); +} + +/* + * Usually this function is not called because the client destroys the surface + * role (like xdg_toplevel) first and input_method_relay_set_focus() is called + * before wl_surface is destroyed. + */ +static void +handle_focused_surface_destroy(struct wl_listener *listener, void *data) +{ + struct input_method_relay *relay = + wl_container_of(listener, relay, focused_surface_destroy); + assert(relay->focused_surface == data); + + input_method_relay_set_focus(relay, NULL); +} + +struct input_method_relay * +input_method_relay_create(struct seat *seat) +{ + struct input_method_relay *relay = znew(*relay); + relay->seat = seat; + wl_list_init(&relay->text_inputs); + + relay->new_text_input.notify = handle_new_text_input; + wl_signal_add(&seat->server->text_input_manager->events.text_input, + &relay->new_text_input); + + relay->new_input_method.notify = handle_new_input_method; + wl_signal_add(&seat->server->input_method_manager->events.input_method, + &relay->new_input_method); + + relay->focused_surface_destroy.notify = handle_focused_surface_destroy; + relay->popup_surface_destroy.notify = handle_popup_surface_destroy; + relay->popup_surface_commit.notify = handle_popup_surface_commit; + + relay->popup_tree = wlr_scene_tree_create(&seat->server->scene->tree); + + text_input_manager_v1_init(seat->server->wl_display, + seat->server->text_input_manager); + + return relay; +} + +void +input_method_relay_finish(struct input_method_relay *relay) +{ + wl_list_remove(&relay->new_text_input.link); + wl_list_remove(&relay->new_input_method.link); + free(relay); +} + +void +input_method_relay_set_focus(struct input_method_relay *relay, + struct wlr_surface *surface) +{ + if (relay->focused_surface == surface) { + wlr_log(WLR_INFO, "The surface is already focused"); + return; + } + + if (relay->focused_surface) { + wl_list_remove(&relay->focused_surface_destroy.link); + } + relay->focused_surface = surface; + if (surface) { + wl_signal_add(&surface->events.destroy, + &relay->focused_surface_destroy); + } + + update_text_inputs_focused_surface(relay); + update_active_text_input(relay); +} diff --git a/src/input/keyboard.c b/src/input/keyboard.c index 7379a54..3b0b6d9 100644 --- a/src/input/keyboard.c +++ b/src/input/keyboard.c @@ -97,7 +97,11 @@ keyboard_modifiers_notify(struct wl_listener *listener, void *data) } } } - wlr_seat_keyboard_notify_modifiers(seat->seat, &wlr_keyboard->modifiers); + + if (!input_method_keyboard_grab_forward_modifiers(keyboard)) { + wlr_seat_keyboard_notify_modifiers(seat->seat, + &wlr_keyboard->modifiers); + } } static struct keybind * @@ -523,7 +527,7 @@ keyboard_key_notify(struct wl_listener *listener, void *data) if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) { start_keybind_repeat(seat->server, keyboard, event); } - } else { + } else if (!input_method_keyboard_grab_forward_key(keyboard, event)) { wlr_seat_set_keyboard(wlr_seat, keyboard->wlr_keyboard); wlr_seat_keyboard_notify_key(wlr_seat, event->time_msec, event->keycode, event->state); diff --git a/src/input/meson.build b/src/input/meson.build index c8b7717..c333184 100644 --- a/src/input/meson.build +++ b/src/input/meson.build @@ -7,4 +7,5 @@ labwc_sources += files( 'keyboard.c', 'key-state.c', 'touch.c', + 'ime.c', ) diff --git a/src/layers.c b/src/layers.c index 20e9e68..f84c2fb 100644 --- a/src/layers.c +++ b/src/layers.c @@ -298,6 +298,8 @@ create_popup(struct wlr_xdg_popup *wlr_popup, struct wlr_scene_tree *parent) free(popup); return NULL; } + /* In support of IME popups */ + popup->wlr_popup->base->surface->data = popup->scene_tree; node_descriptor_create(&popup->scene_tree->node, LAB_NODE_DESC_LAYER_POPUP, popup); @@ -424,6 +426,9 @@ handle_new_layer_surface(struct wl_listener *listener, void *data) return; } + /* In support of IME popups */ + layer_surface->surface->data = surface->scene_layer_surface->tree; + node_descriptor_create(&surface->scene_layer_surface->tree->node, LAB_NODE_DESC_LAYER_SURFACE, surface); diff --git a/src/output.c b/src/output.c index e1c615e..5eca379 100644 --- a/src/output.c +++ b/src/output.c @@ -320,6 +320,7 @@ new_output_notify(struct wl_listener *listener, void *data) /* * Set the z-positions to achieve the following order (from top to * bottom): + * - IME popups * - session lock layer * - layer-shell popups * - overlay layer @@ -334,6 +335,7 @@ new_output_notify(struct wl_listener *listener, void *data) wlr_scene_node_raise_to_top(&output->layer_tree[3]->node); wlr_scene_node_raise_to_top(&output->layer_popup_tree->node); wlr_scene_node_raise_to_top(&output->session_lock_tree->node); + wlr_scene_node_raise_to_top(&server->seat.input_method_relay->popup_tree->node); /* * Wait until wlr_output_layout_add_auto() returns before diff --git a/src/seat.c b/src/seat.c index e4e69ef..27d5d57 100644 --- a/src/seat.c +++ b/src/seat.c @@ -536,6 +536,8 @@ seat_init(struct server *server) &seat->virtual_keyboard_new); seat->virtual_keyboard_new.notify = new_virtual_keyboard; + seat->input_method_relay = input_method_relay_create(seat); + seat->cursor = wlr_cursor_create(); if (!seat->cursor) { wlr_log(WLR_ERROR, "unable to create cursor"); @@ -559,6 +561,7 @@ seat_finish(struct server *server) } input_handlers_finish(seat); + input_method_relay_finish(seat->input_method_relay); } static void @@ -614,6 +617,7 @@ seat_focus(struct seat *seat, struct wlr_surface *surface, bool is_lock_surface) if (!surface) { wlr_seat_keyboard_notify_clear_focus(seat->seat); + input_method_relay_set_focus(seat->input_method_relay, NULL); return; } @@ -636,6 +640,8 @@ seat_focus(struct seat *seat, struct wlr_surface *surface, bool is_lock_surface) wlr_seat_keyboard_notify_enter(seat->seat, surface, pressed_sent_keycodes, nr_pressed_sent_keycodes, &kb->modifiers); + input_method_relay_set_focus(seat->input_method_relay, surface); + struct wlr_pointer_constraint_v1 *constraint = wlr_pointer_constraints_v1_constraint_for_surface(server->constraints, surface, seat->seat); diff --git a/src/server.c b/src/server.c index c0ee768..e88b25e 100644 --- a/src/server.c +++ b/src/server.c @@ -378,6 +378,10 @@ server_init(struct server *server) */ wlr_primary_selection_v1_device_manager_create(server->wl_display); + server->input_method_manager = wlr_input_method_manager_v2_create( + server->wl_display); + server->text_input_manager = wlr_text_input_manager_v3_create( + server->wl_display); seat_init(server); xdg_shell_init(server); kde_server_decoration_init(server); diff --git a/src/xdg.c b/src/xdg.c index dc9acc9..8a96807 100644 --- a/src/xdg.c +++ b/src/xdg.c @@ -771,7 +771,7 @@ xdg_surface_new(struct wl_listener *listener, void *data) */ kde_server_decoration_set_view(view, xdg_surface->surface); - /* In support of xdg popups */ + /* In support of xdg popups and IME popups */ xdg_surface->surface->data = tree; view_connect_map(view, xdg_surface->surface);