diff --git a/text-input-unstable-v3.xml b/text-input-unstable-v3.xml new file mode 100644 index 0000000..1fae54d --- /dev/null +++ b/text-input-unstable-v3.xml @@ -0,0 +1,457 @@ + + + + + Copyright © 2012, 2013 Intel Corporation + Copyright © 2015, 2016 Jan Arne Petersen + Copyright © 2017, 2018 Red Hat, Inc. + Copyright © 2018 Purism SPC + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol allows compositors to act as input methods and to send text + to applications. A text input object is used to manage state of what are + typically text entry fields in the application. + + This document adheres to the RFC 2119 when using words like "must", + "should", "may", etc. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + The zwp_text_input_v3 interface represents text input and input methods + associated with a seat. It provides enter/leave events to follow the + text input focus for a seat. + + Requests are used to enable/disable the text-input object and set + state information like surrounding and selected text or the content type. + The information about the entered text is sent to the text-input object + via the preedit_string and commit_string events. + + Text is valid UTF-8 encoded, indices and lengths are in bytes. Indices + must not point to middle bytes inside a code point: they must either + point to the first byte of a code point or to the end of the buffer. + Lengths must be measured between two valid indices. + + Focus moving throughout surfaces will result in the emission of + zwp_text_input_v3.enter and zwp_text_input_v3.leave events. The focused + surface must commit zwp_text_input_v3.enable and + zwp_text_input_v3.disable requests as the keyboard focus moves across + editable and non-editable elements of the UI. Those two requests are not + expected to be paired with each other, the compositor must be able to + handle consecutive series of the same request. + + State is sent by the state requests (set_surrounding_text, + set_content_type and set_cursor_rectangle) and a commit request. After an + enter event or disable request all state information is invalidated and + needs to be resent by the client. + + + + + Destroy the wp_text_input object. Also disables all surfaces enabled + through this wp_text_input object. + + + + + + Requests text input on the surface previously obtained from the enter + event. + + This request must be issued every time the active text input changes + to a new one, including within the current surface. Use + zwp_text_input_v3.disable when there is no longer any input focus on + the current surface. + + Clients must not enable more than one text input on the single seat + and should disable the current text input before enabling the new one. + At most one instance of text input may be in enabled state per instance, + Requests to enable the another text input when some text input is active + must be ignored by compositor. + + This request resets all state associated with previous enable, disable, + set_surrounding_text, set_text_change_cause, set_content_type, and + set_cursor_rectangle requests, as well as the state associated with + preedit_string, commit_string, and delete_surrounding_text events. + + The set_surrounding_text, set_content_type and set_cursor_rectangle + requests must follow if the text input supports the necessary + functionality. + + State set with this request is double-buffered. It will get applied on + the next zwp_text_input_v3.commit request, and stay valid until the + next committed enable or disable request. + + The changes must be applied by the compositor after issuing a + zwp_text_input_v3.commit request. + + + + + + Explicitly disable text input on the current surface (typically when + there is no focus on any text entry inside the surface). + + State set with this request is double-buffered. It will get applied on + the next zwp_text_input_v3.commit request. + + + + + + Sets the surrounding plain text around the input, excluding the preedit + text. + + The client should notify the compositor of any changes in any of the + values carried with this request, including changes caused by handling + incoming text-input events as well as changes caused by other + mechanisms like keyboard typing. + + If the client is unaware of the text around the cursor, it should not + issue this request, to signify lack of support to the compositor. + + Text is UTF-8 encoded, and should include the cursor position, the + complete selection and additional characters before and after them. + There is a maximum length of wayland messages, so text can not be + longer than 4000 bytes. + + Cursor is the byte offset of the cursor within text buffer. + + Anchor is the byte offset of the selection anchor within text buffer. + If there is no selected text, anchor is the same as cursor. + + If any preedit text is present, it is replaced with a cursor for the + purpose of this event. + + Values set with this request are double-buffered. They will get applied + on the next zwp_text_input_v3.commit request, and stay valid until the + next committed enable or disable request. + + The initial state for affected fields is empty, meaning that the text + input does not support sending surrounding text. If the empty values + get applied, subsequent attempts to change them may have no effect. + + + + + + + + + Reason for the change of surrounding text or cursor posision. + + + + + + + + Tells the compositor why the text surrounding the cursor changed. + + Whenever the client detects an external change in text, cursor, or + anchor posision, it must issue this request to the compositor. This + request is intended to give the input method a chance to update the + preedit text in an appropriate way, e.g. by removing it when the user + starts typing with a keyboard. + + cause describes the source of the change. + + The value set with this request is double-buffered. It must be applied + and reset to initial at the next zwp_text_input_v3.commit request. + + The initial value of cause is input_method. + + + + + + + Content hint is a bitmask to allow to modify the behavior of the text + input. + + + + + + + + + + + + + + + + + The content purpose allows to specify the primary purpose of a text + input. + + This allows an input method to show special purpose input panels with + extra characters or to disallow some characters. + + + + + + + + + + + + + + + + + + + + Sets the content purpose and content hint. While the purpose is the + basic purpose of an input field, the hint flags allow to modify some of + the behavior. + + Values set with this request are double-buffered. They will get applied + on the next zwp_text_input_v3.commit request. + Subsequent attempts to update them may have no effect. The values + remain valid until the next committed enable or disable request. + + The initial value for hint is none, and the initial value for purpose + is normal. + + + + + + + + Marks an area around the cursor as a x, y, width, height rectangle in + surface local coordinates. + + Allows the compositor to put a window with word suggestions near the + cursor, without obstructing the text being input. + + If the client is unaware of the position of edited text, it should not + issue this request, to signify lack of support to the compositor. + + Values set with this request are double-buffered. They will get applied + on the next zwp_text_input_v3.commit request, and stay valid until the + next committed enable or disable request. + + The initial values describing a cursor rectangle are empty. That means + the text input does not support describing the cursor area. If the + empty values get applied, subsequent attempts to change them may have + no effect. + + + + + + + + + + Atomically applies state changes recently sent to the compositor. + + The commit request establishes and updates the state of the client, and + must be issued after any changes to apply them. + + Text input state (enabled status, content purpose, content hint, + surrounding text and change cause, cursor rectangle) is conceptually + double-buffered within the context of a text input, i.e. between a + committed enable request and the following committed enable or disable + request. + + Protocol requests modify the pending state, as opposed to the current + state in use by the input method. A commit request atomically applies + all pending state, replacing the current state. After commit, the new + pending state is as documented for each related request. + + Requests are applied in the order of arrival. + + Neither current nor pending state are modified unless noted otherwise. + + The compositor must count the number of commit requests coming from + each zwp_text_input_v3 object and use the count as the serial in done + events. + + + + + + Notification that this seat's text-input focus is on a certain surface. + + If client has created multiple text input objects, compositor must send + this event to all of them. + + When the seat has the keyboard capability the text-input focus follows + the keyboard focus. This event sets the current surface for the + text-input object. + + + + + + + Notification that this seat's text-input focus is no longer on a + certain surface. The client should reset any preedit string previously + set. + + The leave notification clears the current surface. It is sent before + the enter notification for the new focus. After leave event, compositor + must ignore requests from any text input instances until next enter + event. + + When the seat has the keyboard capability the text-input focus follows + the keyboard focus. + + + + + + + Notify when a new composing text (pre-edit) should be set at the + current cursor position. Any previously set composing text must be + removed. Any previously existing selected text must be removed. + + The argument text contains the pre-edit string buffer. + + The parameters cursor_begin and cursor_end are counted in bytes + relative to the beginning of the submitted text buffer. Cursor should + be hidden when both are equal to -1. + + They could be represented by the client as a line if both values are + the same, or as a text highlight otherwise. + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.done event. + + The initial value of text is an empty string, and cursor_begin, + cursor_end and cursor_hidden are all 0. + + + + + + + + + Notify when text should be inserted into the editor widget. The text to + commit could be either just a single character after a key press or the + result of some composing (pre-edit). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.done event. + + The initial value of text is an empty string. + + + + + + + Notify when the text around the current cursor position should be + deleted. + + Before_length and after_length are the number of bytes before and after + the current cursor index (excluding the selection) to delete. + + If a preedit text is present, in effect before_length is counted from + the beginning of it, and after_length from its end (see done event + sequence). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.done event. + + The initial values of both before_length and after_length are 0. + + + + + + + + Instruct the application to apply changes to state requested by the + preedit_string, commit_string and delete_surrounding_text events. The + state relating to these events is double-buffered, and each one + modifies the pending state. This event replaces the current state with + the pending state. + + The application must proceed by evaluating the changes in the following + order: + + 1. Replace existing preedit string with the cursor. + 2. Delete requested surrounding text. + 3. Insert commit string with the cursor at its end. + 4. Calculate surrounding text to send. + 5. Insert new preedit text in cursor position. + 6. Place cursor inside preedit text. + + The serial number reflects the last state of the zwp_text_input_v3 + object known to the compositor. The value of the serial argument must + be equal to the number of commit requests already issued on that object. + + When the client receives a done event with a serial different than the + number of past commit requests, it must proceed with evaluating and + applying the changes as normal, except it should not change the current + state of the zwp_text_input_v3 object. All pending state requests + (set_surrounding_text, set_content_type and set_cursor_rectangle) on + the zwp_text_input_v3 object should be sent and committed after + receiving a zwp_text_input_v3.done event with a matching serial. + + + + + + + + A factory for text-input objects. This object is a global singleton. + + + + + Destroy the wp_text_input_manager object. + + + + + + Creates a new text-input object for a given seat. + + + + + + diff --git a/text_input.c b/text_input.c new file mode 100644 index 0000000..9d173ac --- /dev/null +++ b/text_input.c @@ -0,0 +1,3395 @@ +/* Wayland compositor running on top of an X server. + +Copyright (C) 2022 to various contributors. + +This file is part of 12to11. + +12to11 is free software: you can redistribute it and/or modify it +under the terms of the GNU General Public License as published by the +Free Software Foundation, either version 3 of the License, or (at your +option) any later version. + +12to11 is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with 12to11. If not, see . */ + +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "compositor.h" +#include "text-input-unstable-v3.h" + +#include + +/* X Input Method (XIM) support. + + The X input method has a client-server architecture; the connection + between the client and server is abstracted away by Xlib, and + always results in an XIM object being produced. The connection + itself can take many forms: the IM server could be linked into the + X library, another X client on the same server, running on another + machine over a TCP/IP connection, or even a DECnet node. + + The XIM object will be assigned an arbitrary seat (usually the + virtual core keyboard), which will be the only seat that can + utilize input methods. + + Each text input will have a corresponding input context (XIC) for + every focused window. The XIC handles state logically associated + with a single text entry area, such as currently composed text, the + currently focused surface, the position of the cursor, and text + surrounding the cursor. + + When the previously assigned seat's focus moves to a surface with + an associated XIC, and the text input is enabled, focus is given to + the XIC. Subsequent extension key events are translated into core + ones, then sent to the input context; then, should the input + context chose to discard the event, the event is simply discarded. + Otherwise, XmbLookupString is called on the event, and any keysym + or string returned is looked up and committed or sent to the + surface. + + The X library synthesizes fake client-side events to represent + XIM_COMMIT events, and saves the committed text for XmbLookupString + to return. These events do not contain the information necessary + to determine the XIC that sent the event which caused it to be + generated. Similarly, events sent with XIM_FORWARD_EVENT do not + contain enough information to attribute them to the XIC that sent + them. + + That means it is impossible to attribute forwarded events or + committed text to the correct XIC, and thus it is impossible to + look up which seat's TextInput resource an event is actually bound + for. If one day we move to our own implementation of the XIC + protocol, then it will become possible to properly support + multi-seat setups, with one XIC per-client and per-seat. + + Further more, the XIM has its own locale, which is not guaranteed + to have the same coded character set as the character set used in + the Wayland protocol, namely UTF-8. During XIM creation, its + locale's coded character set is obtained and used to create a + character conversion context. All text obtained from the XIM + callbacks is then converted with that context, and character + indices provided by the XIM are converted to byte indices into the + converted string before being sent to the client. + + This code has many inherent race conditions, just like the + zwp_text_input_v3 protocol itself. As described above, it only + supports one seat due to limitations of the Xlib XIM wrapper. */ + +typedef struct _TextInputClientInfo TextInputClientInfo; +typedef struct _TextInput TextInput; +typedef struct _TextInputState TextInputState; +typedef struct _TextPosition TextPosition; +typedef struct _PreeditBuffer PreeditBuffer; + +typedef enum _XimStyleKind XimStyleKind; + +enum _XimStyleKind + { + XimStyleNone, + XimOverTheSpot, + XimOffTheSpot, + XimRootWindow, + XimOnTheSpot, + }; + +enum + { + PendingEnabled = 1, + PendingCursorRectangle = (1 << 1), + PendingSurroundingText = (1 << 2), + }; + +struct _PreeditBuffer +{ + /* Buffer data. */ + char *buffer; + + /* The locale. */ + char *locale; + + /* Buffer size in bytes. */ + size_t size; + + /* Buffer size in characters. */ + int total_characters; +}; + +struct _TextPosition +{ + /* Byte position. */ + ptrdiff_t bytepos; + + /* Character position. */ + int charpos; +}; + +struct _TextInputState +{ + /* What is defined; alternatively, what is pending. */ + int pending; + + /* Whether or not this text input is enabled. */ + Bool enabled; + + /* Cursor rectangle. */ + int cursor_x, cursor_y, cursor_width, cursor_height; + + /* Surrounding text. This is allocated with XLMalloc and is made + NULL upon commit in the pending state. */ + char *surrounding_text; + + /* Character and byte positions of the cursor in the surrounding + text. */ + TextPosition cursor; +}; + +struct _TextInput +{ + /* The TextInputClientInfo associated with this text input. */ + TextInputClientInfo *client_info; + + /* The wl_resource associated with this text input. */ + struct wl_resource *resource; + + /* The next and last TextInputs. */ + TextInput *next, *last; + + /* The XIC associated with this text input. */ + XIC xic; + + /* The current pre-edit buffer, or NULL. */ + PreeditBuffer *buffer; + + /* The position of the preedit caret in characters. */ + int caret; + + /* The pending state. */ + TextInputState pending_state; + + /* The current state. */ + TextInputState current_state; + + /* Number of commit requests performed. */ + uint32_t serial; +}; + +/* Structure describing a list of TextInput resources associated with + a given client. */ +struct _TextInputClientInfo +{ + /* The next and last objects in this list. */ + TextInputClientInfo *next, *last; + + /* The associated seat. */ + Seat *seat; + + /* The key associated with the seat destruction callback. */ + void *seat_key; + + /* The associated Wayland client. */ + struct wl_client *client; + + /* The list of Wayland client info objects. */ + TextInput inputs; + + /* The current focused surface. */ + Surface *focus_surface; +}; + +/* List of all TextInputClientInfos. */ +static TextInputClientInfo all_client_infos; + +/* The text input manager global. */ +static struct wl_global *text_input_manager_global; + +/* The IM fontset. */ +static XFontSet im_fontset; + +#if defined DEBUG +#define DebugPrint(format, args...) \ + fprintf (stderr, "%s: " format "\n", __FUNCTION__, ## args) +#else +#define DebugPrint(fmt, ...) ((void) 0) +#endif + +/* The XIM currently in use, or NULL. */ +static XIM current_xim; + +/* The conversion context for that XIM. */ +static iconv_t current_cd; + +/* The preferred XIM style. */ +static XIMStyle xim_style; + +/* The order in which XIM input styles will be searched for. */ +static XimStyleKind xim_style_order[5]; + + +/* Byte-text position conversion. */ + +static int +CountOctets (char byte) +{ + /* Given the start of a UTF-8 sequence, return how many following + bytes are in the current sequence. */ + return (!(byte & 0x80) ? 1 + : !(byte & 0x20) ? 2 + : !(byte & 0x10) ? 3 + : !(byte & 0x08) ? 4 + : 5); +} + +static TextPosition +TextPositionFromBytePosition (const char *string, size_t length, + ptrdiff_t byte_position) +{ + const char *start; + TextPosition position; + + start = string; + position.charpos = 0; + position.bytepos = byte_position; + + if (!byte_position) + return position; + + if (byte_position > length) + goto invalid; + + while (start < string + length) + { + if (start + CountOctets (*start) > string + length) + goto invalid; + + start += CountOctets (*start); + position.charpos++; + position.bytepos = start - string; + + if (position.bytepos == byte_position) + return position; + else if (position.bytepos > byte_position) + goto invalid; + } + + invalid: + /* Return the invalid text position. */ + position.bytepos = -1; + position.charpos = -1; + return position; +} + +static TextPosition +TextPositionFromCharPosition (const char *string, size_t length, + int char_position) +{ + const char *start; + TextPosition position; + + start = string; + position.charpos = 0; + position.bytepos = 0; + + if (!char_position) + return position; + + while (position.charpos < char_position) + { + if (start + CountOctets (*start) > string + length) + goto invalid; + + start += CountOctets (*start); + position.charpos++; + position.bytepos = start - string; + } + + /* Return the resulting text position. */ + return position; + + invalid: + /* Return the invalid text position. */ + position.bytepos = -1; + position.charpos = -1; + return position; +} + + + +/* Forward declaration. */ +static void CreateIC (TextInput *); + +static void +DestroyTextInput (struct wl_client *client, struct wl_resource *resource) +{ + wl_resource_destroy (resource); +} + +static void +Enable (struct wl_client *client, struct wl_resource *resource) +{ + TextInput *input; + + input = wl_resource_get_user_data (resource); + + /* If there is already a string, free it, as the pending flag will + be clobbered below. */ + if (input->pending_state.surrounding_text) + XLFree (input->pending_state.surrounding_text); + input->pending_state.surrounding_text = NULL; + + /* Set the pending state. */ + input->pending_state.pending = PendingEnabled; + input->pending_state.enabled = True; +} + +static void +Disable (struct wl_client *client, struct wl_resource *resource) +{ + TextInput *input; + + input = wl_resource_get_user_data (resource); + + /* If there is already a string, free it, as the pending flag will + be clobbered below. */ + if (input->pending_state.surrounding_text) + XLFree (input->pending_state.surrounding_text); + input->pending_state.surrounding_text = NULL; + + /* Set the pending state. */ + input->pending_state.pending = PendingEnabled; + input->pending_state.enabled = False; +} + +static void +SetSurroundingText (struct wl_client *client, struct wl_resource *resource, + const char *text, int cursor, int anchor) +{ + TextInput *input; + + input = wl_resource_get_user_data (resource); + + /* If there is already a string, free it. */ + if (input->pending_state.surrounding_text) + XLFree (input->pending_state.surrounding_text); + + /* Set the surrounding text and cursor position. */ + input->pending_state.surrounding_text = XLStrdup (text); + input->pending_state.cursor + = TextPositionFromBytePosition (text, strlen (text), + cursor); + input->pending_state.pending |= PendingSurroundingText; +} + +static void +SetTextChangeCause (struct wl_client *client, struct wl_resource *resource, + uint32_t cause) +{ + /* Not supported. */ +} + +static void +SetContentType (struct wl_client *client, struct wl_resource *resource, + uint32_t hint, uint32_t purpose) +{ + /* Not supported. */ +} + +static void +SetCursorRectangle (struct wl_client *client, struct wl_resource *resource, + int32_t x, int32_t y, int32_t width, int32_t height) +{ + TextInput *input; + + input = wl_resource_get_user_data (resource); + + if ((input->current_state.pending & PendingCursorRectangle + /* PendingEnabled will clear the current state's cursor + rectangle. */ + && !input->pending_state.pending & PendingEnabled) + && x == input->current_state.cursor_x + && y == input->current_state.cursor_y + && width == input->current_state.cursor_width + && height == input->current_state.cursor_height) + /* Nothing changed, return. */ + return; + + input->pending_state.pending |= PendingCursorRectangle; + input->pending_state.cursor_x = x; + input->pending_state.cursor_y = y; + input->pending_state.cursor_width = width; + input->pending_state.cursor_height = height; +} + +static TextInput * +FindEnabledTextInput (TextInputClientInfo *info) +{ + TextInput *input; + + input = info->inputs.next; + while (input != &info->inputs) + { + if (input->current_state.enabled) + return input; + + input = input->next; + } + + return NULL; +} + +static void +FitRect (XRectangle *input, int view_width, int view_height, + int caret_x, int caret_y, int caret_width, int caret_height) +{ + XRectangle r1, r2, copy; + + copy = *input; + + /* Try to fit the given dimensions into the view. First, + start with the width: + + right edge of view + ^ |------suggested size-------| + ^ caret X, Y + HEIGHT ^ left edge of view + + If the suggested size does not fit, like so: + + ^ |------suggested size------^--| + ^ caret X, Y + HEIGHT + + Move the caret to X 0 so it does, if the suggested size is wider + than half the view: + + ^|------suggested size---------|^ + ^ start X, Y + HEIGHT + + Otherwise, move the rectangle leftwards until it fits. + + If it still does not fit, limit the width to that of the + the view. + + Next, fit the height. Start by placing the rectangle + below the caret. If that is too small, try placing the + rectangle so that its bottom touches the caret. If that + still does not fit, then use the tallest of the following + two rectangles: + + CARET_Y + CARET_HEIGHT by (BOTTOM_X - CARET_X + CARET_HEIGHT + 1) + 0 by CARET_Y. */ + + /* Do the width. Input should already be placed at the bottom right + corner of the caret. */ + + if (input->x + input->width >= view_width) + { + if (input->width > view_width / 2) + /* Flip x to 0. */ + input->x = 0; + else + /* Move the rect left until it fits. */ + input->x -= (input->x + input->width - 1 + - view_width); + + /* If it still doesn't fit, set x to 0 and width to + view_width. */ + if (input->x + input->width >= view_width) + { + input->x = 0; + input->width = view_width; + } + } + + /* Do the height. */ + if (input->y + input->height >= view_height) + { + /* Flip the caret upwards, so the last scanline of the preedit + area is immediately above the first scanline of the + caret. */ + input->y = caret_y - input->height; + + /* If the rectangle is still too small, use the rectangle formed + between the top of the view and the top of the caret, or that + between the bottom of the view and the bottom of the caret, + whichever is larger. */ + if (input->y < 0 || input->y + input->height >= view_height) + { + r1.y = 0; + r1.height = caret_y; + + r2.y = caret_y + caret_height; + r2.height = view_height - r2.y; + + if (r1.height > r2.height) + { + input->y = r1.y; + input->height = r1.height; + } + else + { + input->y = r2.y; + input->height = r2.height; + } + } + } + + /* If the rectangle is still invalid, just fall back to the old + one. */ + if (input->width <= 0 || input->height <= 0) + *input = copy; +} + +static void +DoGeometryAllocation (TextInput *input) +{ + XPoint spot; + XRectangle area, needed; + XVaNestedList attr; + View *view; + char *rc; + + DebugPrint ("doing geometry allocation for %p", input); + + if (!input->xic) + return; + + XLAssert (input->client_info->focus_surface != NULL); + view = input->client_info->focus_surface->view; + + if (xim_style & XIMPreeditPosition) + { + DebugPrint ("IM wants spot values for preedit window"); + + if (input->current_state.pending & PendingCursorRectangle) + { + spot.x = input->current_state.cursor_x; + spot.y = (input->current_state.cursor_y + + input->current_state.cursor_height - 1); + } + else + { + spot.x = 0; + spot.y = 1; + } + + DebugPrint ("using spot: %d, %d", spot.x, spot.y); + attr = XVaCreateNestedList (0, XNSpotLocation, &spot, NULL); + XSetICValues (input->xic, XNPreeditAttributes, attr, NULL); + XFree (attr); + } + else if (xim_style & XIMPreeditArea) + { + DebugPrint ("IM wants geometry negotiation"); + + /* Suggest no size to the input method. */ + area.x = area.y = area.width = area.height = 0; + + attr = XVaCreateNestedList (0, XNAreaNeeded, &area, NULL); + XSetICValues (input->xic, XNPreeditAttributes, attr, NULL); + XFree (attr); + + /* Get the size from the input method. */ + attr = XVaCreateNestedList (0, XNAreaNeeded, &needed, NULL); + rc = XGetICValues (input->xic, XNPreeditAttributes, attr, NULL); + XFree (attr); + + if (!rc) + { + DebugPrint ("IM suggested the given size: %d %d", + needed.width, needed.height); + + /* Place the rectangle below and to the right of the + caret. */ + + if (input->current_state.pending & PendingCursorRectangle) + { + needed.x = (input->current_state.cursor_x + + input->current_state.cursor_width); + needed.y = (input->current_state.cursor_y + + input->current_state.cursor_height); + + FitRect (&needed, ViewWidth (view), ViewHeight (view), + input->current_state.cursor_x, + input->current_state.cursor_y, + input->current_state.cursor_width, + input->current_state.cursor_height); + + DebugPrint ("filled rectangle: %d %d %d %d", + needed.x, needed.y, needed.width, + needed.height); + } + else + { + /* No caret was specified... Place the preedit window on + the bottom right corner of the view. */ + needed.x = ViewWidth (view); + needed.y = ViewHeight (view); + + DebugPrint ("placed rectangle: %d %d %d %d", + needed.x, needed.y, needed.width, + needed.height); + } + + /* Set the geometry. */ + attr = XVaCreateNestedList (0, XNArea, &needed, NULL); + XSetICValues (input->xic, XNPreeditAttributes, attr, NULL); + XFree (attr); + } + } + + if (xim_style & XIMStatusArea) + { + DebugPrint ("IM wants geometry negotiation for status area"); + + /* Suggest no size to the input method. */ + area.x = area.y = area.width = area.height = 0; + + attr = XVaCreateNestedList (0, XNAreaNeeded, &area, NULL); + XSetICValues (input->xic, XNStatusAttributes, attr, NULL); + XFree (attr); + + /* Get the size from the input method. */ + attr = XVaCreateNestedList (0, XNAreaNeeded, &needed, NULL); + rc = XGetICValues (input->xic, XNStatusAttributes, attr, NULL); + XFree (attr); + + if (!rc) + { + DebugPrint ("IM suggested the given size: %d %d", + needed.width, needed.height); + + /* Place the rectangle below and to the right of the + caret. */ + + if (input->current_state.pending & PendingCursorRectangle) + { + needed.x = (input->current_state.cursor_x + + input->current_state.cursor_width); + needed.y = (input->current_state.cursor_y + + input->current_state.cursor_height); + + FitRect (&needed, ViewWidth (view), ViewHeight (view), + input->current_state.cursor_x, + input->current_state.cursor_y, + input->current_state.cursor_width, + input->current_state.cursor_height); + + DebugPrint ("filled rectangle: %d %d %d %d", + needed.x, needed.y, needed.width, + needed.height); + } + else + { + /* No caret was specified... Place the preedit window on + the bottom right corner of the view. */ + needed.x = ViewWidth (view); + needed.y = ViewHeight (view); + + DebugPrint ("placed rectangle: %d %d %d %d", + needed.x, needed.y, needed.width, + needed.height); + } + + /* Set the geometry. */ + attr = XVaCreateNestedList (0, XNArea, &needed, NULL); + XSetICValues (input->xic, XNStatusAttributes, attr, NULL); + XFree (attr); + } + } +} + +static void +Commit (struct wl_client *client, struct wl_resource *resource) +{ + TextInput *input, *enabled; + + input = wl_resource_get_user_data (resource); + input->serial++; + + if (!input->client_info) + /* The text input has no more associated seat. */ + return; + + if (!input->client_info->focus_surface) + /* The text input has no more associated surface. */ + return; + + if (input->pending_state.pending & PendingEnabled) + { + if (input->pending_state.enabled) + { + /* Check if there is another enabled text input in the same + client info structure. */ + enabled = FindEnabledTextInput (input->client_info); + + if (enabled && enabled != input) + /* Return, as the spec says we should ignore requests to + enable a text input. */ + return; + } + + /* Free any surrounding text in the current state. */ + if (input->current_state.surrounding_text) + XLFree (input->current_state.surrounding_text); + + /* Copy the pending state wholesale. */ + input->current_state = input->pending_state; + + /* Clear the surrounding text. */ + input->pending_state.surrounding_text = NULL; + input->pending_state.pending = 0; + + if (input->current_state.surrounding_text) + DebugPrint ("surrounding text early change: %s[%d]", + input->current_state.surrounding_text, + input->current_state.cursor.charpos); + + if (input->current_state.enabled) + { + DebugPrint ("text input %p enabled, state: %2b", input, + (unsigned int) input->current_state.pending); + + /* Maybe create or reset and then focus the IC. */ + if (!input->xic) + CreateIC (input); + else + XFree (XmbResetIC (input->xic)); + + if (input->xic) + XSetICFocus (input->xic); + + /* Perform geometry/position allocation on the IC. */ + DoGeometryAllocation (input); + } + else + { + DebugPrint ("text input %p disabled", input); + + if (input->xic) + XUnsetICFocus (input->xic); + } + } + else + { + /* Apply the pending state piecemeal. */ + if (input->pending_state.pending & PendingCursorRectangle) + { + DebugPrint ("cursor rectangle changed to: %d %d %d %d", + input->pending_state.cursor_x, + input->pending_state.cursor_y, + input->pending_state.cursor_width, + input->pending_state.cursor_height); + + input->current_state.cursor_x + = input->pending_state.cursor_x; + input->current_state.cursor_y + = input->pending_state.cursor_y; + input->current_state.cursor_width + = input->pending_state.cursor_width; + input->current_state.cursor_height + = input->pending_state.cursor_height; + + input->current_state.pending |= PendingCursorRectangle; + + if (input->current_state.enabled && input->xic) + /* Perform geometry/position allocation on the IC. */ + DoGeometryAllocation (input); + } + + if (input->pending_state.pending & PendingSurroundingText) + { + DebugPrint ("surrounding text changed to: %s[%d]", + input->pending_state.surrounding_text, + input->pending_state.cursor.charpos); + + if (input->current_state.surrounding_text) + XLFree (input->current_state.surrounding_text); + + /* Surrounding text changed. Move the surrounding text and + cursor position over. */ + input->current_state.surrounding_text + = input->pending_state.surrounding_text; + input->current_state.cursor = input->pending_state.cursor; + + /* Clear the surrounding text on the pending state. */ + input->pending_state.surrounding_text = NULL; + + /* And add the flag to the current state. */ + input->current_state.pending |= PendingSurroundingText; + } + + /* Clear the pending state mask. */ + input->pending_state.pending = 0; + } +} + +static const struct zwp_text_input_v3_interface input_impl = + { + .destroy = DestroyTextInput, + .enable = Enable, + .disable = Disable, + .set_surrounding_text = SetSurroundingText, + .set_text_change_cause = SetTextChangeCause, + .set_content_type = SetContentType, + .set_cursor_rectangle = SetCursorRectangle, + .commit = Commit, + }; + +/* Forward declarations. */ +static void FreePreeditBuffer (PreeditBuffer *); +static void UpdatePreedit (TextInput *); + +static void +HandleICDestroyed (TextInput *input) +{ + /* Destroy the preedit buffer and update the preedit state. */ + if (input->buffer) + { + FreePreeditBuffer (input->buffer); + input->buffer = NULL; + + /* Send changes to the client. */ + UpdatePreedit (input); + } +} + +static void +InputDoLeave (TextInput *input, Surface *old_surface) +{ + /* Destroy any XIC that was created. */ + if (input->xic) + { + XDestroyIC (input->xic); + input->xic = NULL; + HandleICDestroyed (input); + } + + /* Clear the input state. */ + + if (input->current_state.surrounding_text) + XLFree (input->current_state.surrounding_text); + + memset (&input->current_state, 0, sizeof input->current_state); +} + +static void +InputDoEnter (TextInput *input, Surface *new_surface) +{ + /* If there is still a preedit buffer, destroy it. */ + if (input->buffer) + { + FreePreeditBuffer (input->buffer); + + /* Set it to NULL. */ + input->buffer = NULL; + UpdatePreedit (input); + } +} + +static void +HandleResourceDestroy (struct wl_resource *resource) +{ + TextInput *input; + + input = wl_resource_get_user_data (resource); + + /* Now, if the client info is attached, unlink the text input. */ + if (input->client_info) + { + input->last->next = input->next; + input->next->last = input->last; + + /* If the client info is now empty, destroy the client info as + well. */ + if (input->client_info->inputs.next + == &input->client_info->inputs) + { + XLSeatCancelDestroyListener (input->client_info->seat_key); + input->client_info->last->next = input->client_info->next; + input->client_info->next->last = input->client_info->last; + + XLFree (input->client_info); + } + } + + /* If an XIC still exists, destroy it. */ + if (input->xic) + XDestroyIC (input->xic); + + /* If there is a surrounding text string, free it. */ + if (input->pending_state.surrounding_text) + XLFree (input->pending_state.surrounding_text); + if (input->current_state.surrounding_text) + XLFree (input->current_state.surrounding_text); + + /* If there is still a preedit buffer, destroy it. */ + if (input->buffer) + FreePreeditBuffer (input->buffer); + + /* Free the text input itself. */ + XLFree (input); +} + + + +static void +HandleSeatDestroyed (void *data) +{ + TextInputClientInfo *info; + TextInput *input; + + /* The seat associated with the given TextInputClientInfo was + destroyed. Detach every TextInput object. */ + info = data; + input = info->inputs.next; + + while (input != &info->inputs) + { + input->client_info = NULL; + + /* client_info is now NULL, meaning this text input is inert. + So destroy the XIC, as it's not being destroyed later. */ + if (input->xic) + { + XDestroyIC (input->xic); + input->xic = NULL; + HandleICDestroyed (input); + } + + input = input->next; + } + + /* Next, unlink and free the client info. */ + info->last->next = info->next; + info->next->last = info->last; + XLFree (info); +} + +static void +NoticeEnter (TextInputClientInfo *info, Surface *surface) +{ + TextInput *input; + + DebugPrint ("client info: %p, surface: %p", + info, surface); + + if (info->focus_surface == surface) + /* The focus surface did not change. */ + return; + + input = info->inputs.next; + while (input != &info->inputs) + { + /* If a previous surface exists, also send a leave event. */ + if (info->focus_surface) + { + DebugPrint ("sending leave to text input %p", input); + + XLAssert (info->focus_surface->resource != NULL); + zwp_text_input_v3_send_leave (input->resource, + info->focus_surface->resource); + + InputDoLeave (input, info->focus_surface); + } + + DebugPrint ("sending enter to text input %p", input); + + /* Send the enter event to each text input. */ + zwp_text_input_v3_send_enter (input->resource, + surface->resource); + InputDoEnter (input, surface); + + input = input->next; + } + + /* Record the focus surface. Note that this surface should always + be removed by ClearFocusSurface in the seat upon destruction, so + there is no need for a callback to be registered here as well! + + If that invariant is broken, strange bugs will follow. */ + info->focus_surface = surface; +} + +static void +NoticeLeave (TextInputClientInfo *info) +{ + TextInput *input; + + /* If there is already no focus surface, return. */ + if (!info->focus_surface) + return; + + DebugPrint ("client info: %p", info); + + input = info->inputs.next; + while (input != &info->inputs) + { + DebugPrint ("sending leave to text input %p", input); + + /* Otherwise, if info->focus_surface->resource is still + there, send the leave event to each text input. */ + if (info->focus_surface->resource) + /* Send the enter event to each text input. */ + zwp_text_input_v3_send_leave (input->resource, + info->focus_surface->resource); + InputDoLeave (input, info->focus_surface); + + input = input->next; + } + + /* And clear the focus surface. */ + info->focus_surface = NULL; +} + +static TextInputClientInfo * +GetClientInfo (struct wl_client *client, Seat *seat, Bool create) +{ + TextInputClientInfo *info; + + /* First, look through the list of client infos. */ + info = all_client_infos.next; + + while (info != &all_client_infos) + { + if (info->seat == seat && info->client == client) + return info; + + info = info->next; + } + + if (!create) + return NULL; + + /* If none was found, create one and link it onto the list. */ + info = XLCalloc (1, sizeof *info); + info->seat = seat; + info->client = client; + info->next = all_client_infos.next; + info->last = &all_client_infos; + all_client_infos.next->last = info; + all_client_infos.next = info; + + /* Then, attach the seat destruction listener and initialize the + list of text input objects. */ + info->seat_key + = XLSeatRunOnDestroy (seat, HandleSeatDestroyed, info); + info->inputs.next = &info->inputs; + + /* And return info. */ + return info; +} + +static void +Destroy (struct wl_client *client, struct wl_resource *resource) +{ + wl_resource_destroy (resource); +} + +/* Forward declaration. */ +static void FocusInCallback (Seat *, Surface *); + +static void +GetTextInput (struct wl_client *client, struct wl_resource *resource, + uint32_t id, struct wl_resource *seat_resource) +{ + Seat *seat; + struct wl_resource *dummy; + TextInput *input; + TextInputClientInfo *info; + + seat = wl_resource_get_user_data (seat_resource); + + /* If the seat is inert, we cannot rely on destroy callbacks being + run. In that case, we make a dummy text input resource with no + data attached. */ + if (XLSeatIsInert (seat)) + { + dummy = wl_resource_create (client, &zwp_text_input_v3_interface, + wl_resource_get_version (resource), id); + + if (!dummy) + wl_resource_post_no_memory (resource); + else + wl_resource_set_implementation (dummy, &input_impl, NULL, NULL); + + return; + } + + /* Create the text input. */ + input = XLSafeMalloc (sizeof *input); + + if (!input) + { + wl_resource_post_no_memory (resource); + return; + } + + memset (input, 0, sizeof *input); + input->resource + = wl_resource_create (client, &zwp_text_input_v3_interface, + wl_resource_get_version (resource), id); + + if (!input->resource) + { + XLFree (input); + wl_resource_post_no_memory (resource); + return; + } + + /* Obtain the client info. */ + info = GetClientInfo (client, seat, True); + + /* Set the implementation. N.B. that HandleResourceDestroy will + free the client info structure once all references are gone. */ + wl_resource_set_implementation (input->resource, &input_impl, + input, HandleResourceDestroy); + + /* Initialize and link the text input. */ + input->client_info = info; + input->next = info->inputs.next; + input->last = &info->inputs; + info->inputs.next->last = input; + info->inputs.next = input; + + /* If there is already a focused surface on the seat belonging to + the client, focus it now. */ + if (info->focus_surface) + { + DebugPrint ("focusing newly created text input %p", input); + + /* The info already existed and already has a focus surface + set. */ + zwp_text_input_v3_send_enter (input->resource, + info->focus_surface->resource); + InputDoEnter (input, info->focus_surface); + } + else if (XLSeatIsClientFocused (seat, client)) + { + DebugPrint ("focusing newly created text input with info %p", input); + + /* The info did not previously exist, but the client created a + surface that is the seat's input focus. */ + FocusInCallback (seat, XLSeatGetFocus (seat)); + } +} + +static const struct zwp_text_input_manager_v3_interface manager_impl = + { + .destroy = Destroy, + .get_text_input = GetTextInput, + }; + +static void +HandleBind (struct wl_client *client, void *data, uint32_t version, + uint32_t id) +{ + struct wl_resource *resource; + + resource = wl_resource_create (client, + &zwp_text_input_manager_v3_interface, + version, id); + + if (!resource) + { + wl_client_post_no_memory (client); + return; + } + + wl_resource_set_implementation (resource, &manager_impl, + NULL, NULL); +} + + + +static PreeditBuffer * +MakePreeditBuffer (const char *locale) +{ + PreeditBuffer *buffer; + + buffer = XLCalloc (1, sizeof *buffer); + buffer->locale = XLStrdup (locale); + + return buffer; +} + +static void +FreePreeditBuffer (PreeditBuffer *buffer) +{ + XLFree (buffer->buffer); + XLFree (buffer->locale); + XLFree (buffer); +} + +static Bool +PreeditDeleteChars (PreeditBuffer *buffer, int start_char, + int length) +{ + wchar_t wc; + char *oldlocale, *start, *end; + int rc, chars, old_chars; + + /* Note that preedit buffers operate on text encoded with the IM + locale's charset. Record the old locale. */ + oldlocale = XLStrdup (setlocale (LC_CTYPE, NULL)); + + /* Switch to the new locale. */ + if (!setlocale (LC_CTYPE, buffer->locale)) + { + XLFree (oldlocale); + return False; + } + + start = buffer->buffer; + chars = 0; + + /* Increase start until we reach start_char. */ + while (chars < start_char) + { + if (start >= buffer->buffer + buffer->size) + { + DebugPrint ("start %p out of bounds %p", + start, buffer->buffer + buffer->size); + + /* start is out of bounds. */ + goto failure; + } + + /* After this, rc should either be -1 (meaning failure) or the + number of bytes read. */ + rc = mbtowc (&wc, start, buffer->buffer + buffer->size - start); + chars++; + + DebugPrint ("mbtowc gave (calculating start) %d", rc); + + /* If rc is not -1, move start forward by that much. */ + if (rc != -1) + start += rc; + else + goto failure; + } + + DebugPrint ("chars: %d, start, %p", chars, start); + + /* Now, start is the first byte of the area we want to delete. + Count forward by length. */ + end = start; + old_chars = chars; + + while (chars < old_chars + length) + { + if (end >= buffer->buffer + buffer->size) + { + DebugPrint ("end %p out of bounds %p", + end, buffer->buffer + buffer->size); + + /* end is out of bounds. */ + goto failure; + } + + /* After this, rc should either be -1 (meaning failure) or the + number of bytes read. */ + rc = mbtowc (&wc, end, buffer->buffer + buffer->size - end); + chars++; + + DebugPrint ("mbtowc gave (calculating end) %d", rc); + + /* If rc is not -1, move start forward by that much. */ + if (rc != -1) + end += rc; + else + goto failure; + } + + DebugPrint ("chars: %d, end, %p", chars, end); + + /* Now, delete the area between start and end, by moving the bytes + between end and the end of the buffer to start. */ + memmove (start, end, buffer->buffer + buffer->size - end); + + /* Resize the buffer. */ + buffer->size -= end - start; + buffer->total_characters -= length; + XLAssert (buffer->size >= 0); + + buffer->buffer = XLRealloc (buffer->buffer, + buffer->size); + + /* Restore the locale and return success. */ + setlocale (LC_CTYPE, oldlocale); + XLFree (oldlocale); + + /* Reset the shift state. */ + mbtowc (NULL, NULL, 0); + return True; + + failure: + setlocale (LC_CTYPE, oldlocale); + XLFree (oldlocale); + + /* Reset the shift state. */ + mbtowc (NULL, NULL, 0); + return False; +} + +static Bool +PreeditInsertChars (PreeditBuffer *buffer, int start_char, + const char *string, size_t length, + int char_length) +{ + wchar_t wc; + char *oldlocale, *start; + int rc, chars; + + /* Note that preedit buffers operate on text encoded with the IM + locale's charset. Record the old locale. */ + oldlocale = XLStrdup (setlocale (LC_CTYPE, NULL)); + + /* Switch to the new locale. */ + if (!setlocale (LC_CTYPE, buffer->locale)) + { + XLFree (oldlocale); + return False; + } + + /* Resize the buffer accordingly. */ + buffer->buffer = XLRealloc (buffer->buffer, + buffer->size + length); + + start = buffer->buffer; + chars = 0; + + /* Increase start until we reach start_char. */ + while (chars < start_char) + { + if (start >= buffer->buffer + buffer->size) + /* start is out of bounds. */ + goto failure; + + /* After this, rc should either be -1 (meaning failure) or the + number of bytes read. */ + rc = mbtowc (&wc, start, buffer->buffer + buffer->size - start); + chars++; + + /* If rc is not -1, move start forward by that much. */ + if (rc != -1) + start += rc; + else + goto failure; + } + + /* Move everything past start length away. */ + memmove (start + length, start, + buffer->buffer + buffer->size - start); + buffer->size += length; + buffer->total_characters += char_length; + + /* Copy the text onto start. */ + memcpy (start, string, length); + + setlocale (LC_CTYPE, oldlocale); + XLFree (oldlocale); + + /* Reset the shift state. */ + mbtowc (NULL, NULL, 0); + return True; + + failure: + setlocale (LC_CTYPE, oldlocale); + XLFree (oldlocale); + + /* Reset the shift state. */ + mbtowc (NULL, NULL, 0); + return False; +} + +/* Forward declarations. */ +static char *ConvertString (char *, size_t, size_t *); +static void PreeditString (TextInput *, const char *, size_t, ptrdiff_t); + +static void +UpdatePreedit (TextInput *input) +{ + char *buffer; + size_t new_text_size; + TextPosition caret; + + if (input->buffer) + { + /* Convert the preedit text. */ + buffer = ConvertString (input->buffer->buffer, + input->buffer->size, + &new_text_size); + DebugPrint ("updated buffer %p", buffer); + + if (!buffer) + goto no_buffer; + + /* Obtain the caret position. */ + caret = TextPositionFromCharPosition (buffer, new_text_size, + input->caret); + DebugPrint ("caret position is: char %d, byte: %td", + caret.charpos, caret.bytepos); + + PreeditString (input, buffer, new_text_size, + /* caret.bytepos will be -1 if obtaining the + position failed. */ + caret.bytepos); + XLFree (buffer); + } + else + { + no_buffer: + DebugPrint ("no buffer"); + + /* Clear the preedit string. */ + zwp_text_input_v3_send_preedit_string (input->resource, NULL, + -1, -1); + zwp_text_input_v3_send_done (input->resource, input->serial); + } +} + +static int +PreeditStartCallback (XIC ic, XPointer client_data, XPointer call_data) +{ + TextInput *input; + const char *locale; + + XLAssert (current_xim != NULL); + + input = (TextInput *) client_data; + locale = XLocaleOfIM (current_xim); + + DebugPrint ("text input: %p; locale: %s", input, locale); + + if (input->buffer) + FreePreeditBuffer (input->buffer); + + /* Create the preedit buffer. */ + input->buffer = MakePreeditBuffer (locale); + + /* There should be no limit on the number of bytes in a preedit + string. We make the string fit in 4000 bytes ourselves. */ + return -1; +} + +static void +PreeditDoneCallback (XIC ic, XPointer client_data, XPointer call_data) +{ + TextInput *input; + + input = (TextInput *) client_data; + DebugPrint ("text input: %p", input); + + if (input->buffer) + FreePreeditBuffer (input->buffer); + input->buffer = NULL; + + /* Send change to the client. */ + UpdatePreedit (input); +} + +static void +PreeditDrawCallback (XIC ic, XPointer client_data, + XIMPreeditDrawCallbackStruct *call_data) +{ + TextInput *input; + size_t string_size; + + input = (TextInput *) client_data; + DebugPrint ("text input: %p", input); + + if (!input->buffer) + return; + + DebugPrint ("chg_first: %d, chg_length: %d", + call_data->chg_first, + call_data->chg_length); + + if (call_data->text + && call_data->text->encoding_is_wchar) + { + DebugPrint ("wchar encoding not yet implemented"); + return; + } + + /* Delete text between chg_first and chg_first + chg_length. */ + if (call_data->chg_length + && !PreeditDeleteChars (input->buffer, call_data->chg_first, + call_data->chg_length)) + { + DebugPrint ("text deletion failed"); + return; + } + + if (call_data->text) + { + /* The multibyte string should be NULL terminated. */ + string_size = strlen (call_data->text->string.multi_byte); + + DebugPrint ("inserting text of size %d, %zu", + call_data->text->length, string_size); + + /* Now, insert whatever text was specified at chg_first. */ + if (!PreeditInsertChars (input->buffer, call_data->chg_first, + call_data->text->string.multi_byte, + string_size, call_data->text->length)) + DebugPrint ("insertion failed"); + } + + /* Now set the caret position. */ + input->caret = call_data->caret; + + DebugPrint ("buffer text is now: %.*s, with the caret at %d", + (int) input->buffer->size, input->buffer->buffer, + input->caret); + + /* Send change to the client. */ + UpdatePreedit (input); +} + +static void +PreeditCaretCallback (XIC ic, XPointer client_data, + XIMPreeditCaretCallbackStruct *call_data) +{ + TextInput *input; + + input = (TextInput *) client_data; + + if (!input->buffer) + return; + + DebugPrint ("text input: %p; direction: %u", input, + call_data->direction); + + switch (call_data->direction) + { + case XIMAbsolutePosition: + input->caret = call_data->position; + break; + + case XIMForwardChar: + input->caret = MIN (input->caret + 1, + input->buffer->total_characters); + break; + + case XIMBackwardChar: + input->caret = MAX (input->caret - 1, 0); + break; + + /* The rest cannot be implemented under Wayland as the text + input protocol is too limited. */ + default: + DebugPrint ("unsupported movement direction"); + } + + /* Return the caret position. */ + call_data->position = input->caret; + + /* Send change to the client. */ + UpdatePreedit (input); +} + +static TextPosition +ScanForwardWord (const char *string, size_t string_size, + TextPosition caret, int factor) +{ + const char *start; + Bool punct_found; + TextPosition caret_before; + + start = string + caret.bytepos; + + /* Skip initial whitespace. */ + while (start < string + string_size + /* Make sure the character has 0 trailing bytes. */ + && *start < 127 && *start >= 0 + && (isspace (*start) || ispunct (*start))) + { + start++; + caret.charpos++; + caret.bytepos++; + } + + while (start < string + string_size) + { + punct_found = False; + caret_before = caret; + + start += CountOctets (*start); + + if (start >= string + string_size) + { + /* The string is too big. */ + caret.bytepos = -1; + caret.charpos = -1; + + return caret; + } + else + caret.bytepos = start - string; + + caret.charpos++; + + /* Eat all punctuation. */ + while (isspace (*start) || ispunct (*start)) + { + punct_found = True; + + if (++start >= string + string_size) + /* We are now at the end of the string, so just return the + position of caret_before, which should be before this + extraneous punctuation. */ + return caret_before; + + /* Move the caret forward. */ + caret.charpos++; + caret.bytepos++; + } + + if (punct_found && !(--factor)) + { + /* Punctuation was seen and factor is now 0. Return the + caret before the punctuation. */ + DebugPrint ("returning caret_before: char: %d byte: %td", + caret_before.charpos, caret_before.bytepos); + return caret_before; + } + + /* Simply return the current position at the end of the + string. */ + if (start == string + string_size - 1) + { + DebugPrint ("returning caret_before at end of string: char: %d byte: %td", + caret_before.charpos, caret_before.bytepos); + + return caret_before; + } + } + + return caret; +} + +static Bool +IsLeading (char c) +{ + return (((unsigned char) c) & 0b11000000 + || !(c >> 7)); +} + +static TextPosition +ScanBackwardWord (const char *string, size_t string_size, + TextPosition caret, int factor) +{ + TextPosition original, caret_before; + const char *start; + Bool punct_found; + + /* Record the original caret position. */ + original = caret; + + if (!string_size) + { + /* The string is empty, so simply return the start of the + string. */ + caret.charpos = 0; + caret.bytepos = 0; + return caret; + } + + /* First, skip all whitespace. */ + start = string + caret.bytepos; + while (start >= string + /* Make sure the character has 0 trailing bytes. */ + && *start < 127 && *start >= 0 + && (isspace (*start) || ispunct (*start))) + { + start--; + caret.charpos--; + caret.bytepos--; + + if (caret.charpos <= 0 || caret.bytepos <= 0) + return original; + } + + /* Next, look backwards. Every time whitespace is encountered, + gobble it up, and decrease factor. Once factor is 0, return the + caret position before the first whitespace character. Otherwise, + repeat. */ + + while (start >= string) + { + caret_before = caret; + + do + { + if (--start < string) + { + /* Invalid UTF-8 data was found in STRING. Just return + the start of the string in this case. */ + caret.charpos = 0; + caret.bytepos = 0; + return caret; + } + + caret.bytepos--; + } + while (!IsLeading (*start)); + + caret.charpos--; + + DebugPrint ("caret_before: char: %d byte: %td, new char: %c", + caret_before.charpos, caret_before.bytepos, + *start); + + /* We are now at the start of the last character. If it is + whitespace, eat the whitespace and decrease factor. */ + + punct_found = False; + + while (isspace (*start) || ispunct (*start)) + { + do + { + if (--start < string) + { + /* Invalid UTF-8 data was found in STRING. Just return + the start of the string in this case. */ + caret.charpos = 0; + caret.bytepos = 0; + return caret; + } + + caret.bytepos--; + } + while (!IsLeading (*start)); + caret.charpos--; + + punct_found = True; + } + + if (punct_found && !(--factor)) + { + /* Punctuation was seen and the factor is now 0. Return the + caret before the punctuation. */ + DebugPrint ("returning caret_before: char: %d byte: %td", + caret_before.charpos, caret_before.bytepos); + return caret_before; + } + } + + return caret; +} + +static void +FindTextSections (const char *string, size_t string_size, + TextPosition caret, XIMCaretDirection direction, + int factor, TextPosition *start_return, + TextPosition *end_return) +{ + TextPosition end; + const char *found; + + switch (direction) + { + case XIMForwardChar: + /* Move forward by factor. */ + end = TextPositionFromCharPosition (string, string_size, + caret.charpos + factor); + break; + + case XIMBackwardChar: + /* Move backwards by factor. */ + end = TextPositionFromCharPosition (string, string_size, + MAX (0, caret.charpos - factor)); + break; + + case XIMForwardWord: + /* Move forwards by factor words. */ + end = ScanForwardWord (string, string_size, caret, factor); + break; + + case XIMBackwardWord: + /* Move backwards by factor words. */ + end = ScanBackwardWord (string, string_size, caret, factor); + break; + + case XIMLineStart: + /* Scan backwards for factor newline characters. */ + + found = string + caret.bytepos; + DebugPrint ("start: found %p, found-string %td", + found, found - string); + + while (factor) + { + found = memrchr (string, '\n', found - string); + DebugPrint ("LineStart processing found %p %zd", found, + found - string); + + if (!found) + { + /* Use the beginning of the string. */ + found = string - 1; + + /* Exit the loop too. */ + goto end_line_start; + } + + factor--; + } + + end_line_start: + DebugPrint ("found %p string %p found+1-string %td", + found, string, found + 1 - string); + end = TextPositionFromBytePosition (string, string_size, + found + 1 - string); + break; + + case XIMLineEnd: + /* Scan forwards for factor newline characters. */ + found = string + caret.bytepos; + + while (factor) + { + found = memchr (found + 1, '\n', + (string + string_size - 1) - found + 1); + + if (!found) + { + /* Use the end of the string. */ + found = string + string_size - 1; + goto end_line_end; + } + + factor--; + } + + end_line_end: + end = TextPositionFromBytePosition (string, string_size, + found - 1 - string); + break; + + default: + DebugPrint ("unsuported string conversion direction: %u", + direction); + end.bytepos = 0; + end.charpos = 0; + } + + DebugPrint ("end: char: %d byte: %td", end.charpos, + end.bytepos); + + if (caret.charpos > end.charpos) + *start_return = end, *end_return = caret; + else + *start_return = caret, *end_return = end; +} + +static Bool +MoveCaret (TextPosition *caret, const char *buffer, size_t buffer_size, + int by) +{ + const char *end, *start; + int octets; + + XLAssert (caret->bytepos <= buffer_size); + + if (by > 0) + { + end = buffer + buffer_size; + buffer += caret->bytepos; + + while (by && buffer < end) + { + octets = CountOctets (*buffer); + + /* Move the buffer and text position forwards. */ + buffer += octets; + caret->bytepos += octets; + caret->charpos++; + by--; + } + + /* If caret->bytepos is too large, return failure. */ + if (buffer > end) + return False; + } + else if (by < 0) + { + /* Move the buffer and text position backwards. */ + + start = buffer + caret->bytepos; + while (by && start >= buffer) + { + do + { + start--; + caret->bytepos -= 1; + + if (start < buffer) + return False; + } + while (!IsLeading (*start)); + + caret->charpos--; + by--; + } + } + + return True; +} + +static char * +EncodeIMString (const char *input, size_t input_size, int *chars) +{ + iconv_t cd; + char *oldlocale, *locale; + size_t rc; + ptrdiff_t size; + char *outbuf, *outptr, *end; + size_t outsize, outbytesleft; + int nchars, num_chars_read; + wchar_t wc; + char *inbuf; + + /* Encode the given input string in the IM coding system, and then + return a NULL terminated buffer and the number of characters, or + NULL if the conversion failed. */ + DebugPrint ("encoding string %.*s", (int) input_size, input); + + /* Switch to the input method locale. */ + locale = XLocaleOfIM (current_xim); + oldlocale = XLStrdup (setlocale (LC_CTYPE, NULL)); + + if (!setlocale (LC_CTYPE, locale)) + { + /* Switching to the new locale failed. */ + XLFree (oldlocale); + return NULL; + } + + /* First, try creating a conversion descriptor. */ + cd = iconv_open (nl_langinfo (CODESET), "UTF-8"); + + /* If creating the cd failed, bail out. */ + if (cd == (iconv_t) -1) + { + /* Restore the old locale. */ + if (!setlocale (LC_CTYPE, oldlocale)) + abort (); + + XLFree (oldlocale); + return NULL; + } + + /* Otherwise, start converting. */ + outbuf = XLMalloc (BUFSIZ + 1); + outptr = outbuf; + outsize = BUFSIZ; + outbytesleft = outsize; + inbuf = (char *) input; + + while (input_size > 0) + { + rc = iconv (cd, &inbuf, &input_size, &outptr, + &outbytesleft); + DebugPrint ("iconv gave: %tu", rc); + + if (rc == (size_t) -1) + { + /* See what went wrong. */ + if (errno == E2BIG) + { + /* Reallocate the output buffer. */ + outbuf = XLRealloc (outbuf, outsize + BUFSIZ + 1); + + /* Move the outptr to the right location in the new + outbuf. */ + outptr = outbuf + outsize - outbytesleft; + + /* Expand outsize and outbytesleft. */ + outsize += BUFSIZ; + outbytesleft += BUFSIZ; + + DebugPrint ("expanding outsize to %tu, outbytesleft now %tu", + outsize, outbytesleft); + } + else + { + /* An error occured while encoding the string. + Normally, this is not such a big deal, but the number + of characters in the string is later counted with + mbtowc. So, simply bail out. */ + DebugPrint ("iconv failed: %s", strerror (errno)); + XLFree (outbuf); + iconv_close (cd); + /* Restore the old locale. */ + if (!setlocale (LC_CTYPE, oldlocale)) + abort (); + + XLFree (oldlocale); + return NULL; + } + } + } + + /* The conversion finished. */ + DebugPrint ("conversion finished, size_out %tu", + outsize - outbytesleft); + + /* Now, count the number of multibyte characters. */ + nchars = 0; + end = outbuf; + size = outsize - outbytesleft; + + while (end < outbuf + size) + { + num_chars_read = mbtowc (&wc, end, outbuf + size - end); + nchars++; + + if (num_chars_read != -1) + end += num_chars_read; + else + { + DebugPrint ("mbtowc failed"); + + XLFree (outbuf); + iconv_close (cd); + /* Restore the old locale. */ + if (!setlocale (LC_CTYPE, oldlocale)) + abort (); + + XLFree (oldlocale); + return NULL; + } + } + + /* Reset the shift state and return the number of characters. */ + mbtowc (NULL, NULL, 0); + *chars = nchars; + + /* Close the cd. */ + iconv_close (cd); + + /* Restore the old locale. */ + if (!setlocale (LC_CTYPE, oldlocale)) + abort (); + XLFree (oldlocale); + + /* Return the output buffer. */ + return outbuf; +} + +static void +StringConversionCallback (XIC ic, XPointer client_data, + XIMStringConversionCallbackStruct *call_data) +{ + TextInput *input; + TextPosition start, end, caret; + short position; + size_t length; + char *buffer; + int num_characters; + int bytes_before, bytes_after; + + input = (TextInput *) client_data; + + /* Clear some members of the returned text structure. */ + call_data->text->feedback = NULL; + call_data->text->encoding_is_wchar = False; + + if (!(input->current_state.pending & PendingSurroundingText)) + return; + + DebugPrint ("string conversion; position: %d, factor: %d" + " operation: %u", (short) call_data->position, + call_data->factor, call_data->operation); + + /* Obtain the actual caret position. */ + caret = input->current_state.cursor; + DebugPrint ("current caret position: char: %d, byte: %td", + caret.charpos, caret.bytepos); + + if (caret.charpos < 0 || caret.bytepos < 0) + goto failure; + + /* This is unsigned short in Xlib.h but the spec says it should be + signed. */ + position = (short) call_data->position; + + /* Move the caret by position. */ + length = strlen (input->current_state.surrounding_text); + + /* If the string is too small, just fail. */ + if (!length) + goto failure; + + if (!MoveCaret (&caret, input->current_state.surrounding_text, + length, position)) + { + DebugPrint ("failed to move caret position"); + goto failure; + } + + if (call_data->factor < 1) + goto failure; + + DebugPrint ("new caret position: char %d, byte: %td", + caret.charpos, caret.bytepos); + + /* Now, obtain the start and end of the text to return. */ + FindTextSections (input->current_state.surrounding_text, + length, caret, call_data->direction, + call_data->factor, &start, &end); + + DebugPrint ("start: %d, %td, end: %d, %td", + start.charpos, start.bytepos, end.charpos, + end.bytepos); + + /* If either of those positions are invalid, signal failure. */ + if (start.charpos < 0 || start.bytepos < 0 + || end.charpos < 0 || end.bytepos < 0) + goto failure; + + /* Verify that some assumptions hold. */ + XLAssert (start.bytepos <= end.bytepos && end.bytepos < length); + + /* Extract and encode the contents of the string. */ + buffer = EncodeIMString ((input->current_state.surrounding_text + + start.bytepos), + end.bytepos - start.bytepos + 1, + &num_characters); + + /* Return those characters. */ + + if (buffer) + { + call_data->text->length = MIN (USHRT_MAX, num_characters); + call_data->text->string.mbs = buffer; + } + else + goto failure; + + DebugPrint ("returned text: %s", buffer); + + if (call_data->operation == XIMStringConversionSubstitution) + { + /* Also tell the client to delete the extracted part of the + buffer. First, calculate how much start extends behind the + cursor. This is an approximation; it assumes that the + portion of text to change always contains the caret, which is + not guaranteed to be the case if the IM specified an offset, + direction, and factor that resulted in none of the text + between start and end containing the caret. */ + caret = input->current_state.cursor; + + if (start.bytepos < caret.bytepos) + bytes_before = caret.bytepos - start.bytepos; + else + bytes_before = 0; + + if (end.bytepos > caret.bytepos) + bytes_after = end.bytepos - caret.bytepos; + else + bytes_after = 0; + + DebugPrint ("deleting: %d %d", bytes_before, bytes_after); + + zwp_text_input_v3_send_delete_surrounding_text (input->resource, + bytes_before, + bytes_after); + zwp_text_input_v3_send_done (input->resource, input->serial); + } + + return; + + failure: + /* Return a string of length 0. This assumes XFree is able to free + data allocated with our malloc wrapper. */ + call_data->text->length = 0; + call_data->text->string.mbs = XLMalloc (0); +} + +static void +CreateIC (TextInput *input) +{ + XVaNestedList status_attr, preedit_attr; + XPoint spot; + XRectangle rect; + Window window; + XIMCallback preedit_start_callback; + XIMCallback preedit_draw_callback; + XIMCallback preedit_done_callback; + XIMCallback preedit_caret_callback; + XIMCallback string_conversion_callback; + unsigned long additional_events; + + if (!current_xim) + return; + + if (!input->client_info) + return; + + if (!input->client_info->focus_surface) + return; + + window = XLWindowFromSurface (input->client_info->focus_surface); + + if (!window) + return; + + XLAssert (!input->xic); + + DebugPrint ("creating XIC for text input %p", input); + + status_attr = NULL; + preedit_attr = NULL; + + /* Create an XIC for the given text input. */ + if (xim_style & XIMPreeditPosition) + { + DebugPrint ("IM wants spot values for preedit window"); + + if (input->current_state.pending & PendingCursorRectangle) + { + spot.x = input->current_state.cursor_x; + spot.y = (input->current_state.cursor_y + + input->current_state.cursor_height - 1); + } + else + { + spot.x = 0; + spot.y = 1; + } + + DebugPrint ("using spot: %d, %d", spot.x, spot.y); + preedit_attr = XVaCreateNestedList (0, XNSpotLocation, &spot, + XNFontSet, im_fontset, NULL); + } + else if (xim_style & XIMPreeditArea) + { + DebugPrint ("IM wants geometry negotiation"); + + /* Use some dummy values, and then negotiate geometry after the + XIC is created. */ + rect.x = 0; + rect.y = 0; + rect.height = 1; + rect.width = 1; + + preedit_attr = XVaCreateNestedList (0, XNArea, &rect, XNFontSet, + im_fontset, NULL); + } + else if (xim_style & XIMPreeditCallbacks) + { + DebugPrint ("IM wants preedit callbacks"); + + preedit_start_callback.client_data = (XPointer) input; + preedit_done_callback.client_data = (XPointer) input; + preedit_draw_callback.client_data = (XPointer) input; + preedit_caret_callback.client_data = (XPointer) input; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" + preedit_start_callback.callback = (XIMProc) PreeditStartCallback; + preedit_done_callback.callback = (XIMProc) PreeditDoneCallback; + preedit_draw_callback.callback = (XIMProc) PreeditDrawCallback; + preedit_caret_callback.callback = (XIMProc) PreeditCaretCallback; +#pragma GCC diagnostic pop + + preedit_attr = XVaCreateNestedList (0, XNPreeditStartCallback, + &preedit_start_callback, + XNPreeditDoneCallback, + &preedit_done_callback, + XNPreeditDrawCallback, + &preedit_draw_callback, + XNPreeditCaretCallback, + &preedit_caret_callback, + NULL); + } + + if (xim_style & XIMStatusArea) + { + DebugPrint ("IM wants geometry negotiation for status area"); + + /* Use some dummy values, and then negotiate geometry after the + XIC is created. */ + rect.x = 0; + rect.y = 0; + rect.height = 1; + rect.width = 1; + + status_attr = XVaCreateNestedList (0, XNArea, &rect, XNFontSet, + im_fontset, NULL); + } + + DebugPrint ("preedit attr: %p, status attr: %p", + preedit_attr, status_attr); + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" + string_conversion_callback.client_data = (XPointer) input; + string_conversion_callback.callback = (XIMProc) StringConversionCallback; +#pragma GCC diagnostic pop + + if (preedit_attr && status_attr) + input->xic = XCreateIC (current_xim, XNInputStyle, xim_style, + XNClientWindow, window, XNFocusWindow, + window, XNStatusAttributes, status_attr, + XNPreeditAttributes, preedit_attr, + XNStringConversionCallback, + &string_conversion_callback, + NULL); + else if (preedit_attr) + input->xic = XCreateIC (current_xim, XNInputStyle, xim_style, + XNClientWindow, window, XNFocusWindow, + window, XNPreeditAttributes, preedit_attr, + XNStringConversionCallback, + &string_conversion_callback, + NULL); + else if (status_attr) + input->xic = XCreateIC (current_xim, XNInputStyle, xim_style, + XNClientWindow, window, XNFocusWindow, + window, XNStatusAttributes, status_attr, + XNStringConversionCallback, + &string_conversion_callback, + NULL); + else + input->xic = XCreateIC (current_xim, XNInputStyle, xim_style, + XNClientWindow, window, XNFocusWindow, + window, XNStringConversionCallback, + &string_conversion_callback, + NULL); + + /* Select for additional events should the IC have been successfully + created. Note that we do not deselect for the extra event mask + anywhere; the events an input method makes us select for should + be benign enough. */ + + if (input->xic) + { + additional_events = NoEventMask; + + if (!XGetICValues (input->xic, XNFilterEvents, + &additional_events, NULL) + && additional_events) + { + DebugPrint ("selecting for additional event mask: %lx", + additional_events); + + XLSurfaceSelectExtraEvents (input->client_info->focus_surface, + additional_events); + } + } + + /* Free the nested lists. */ + if (status_attr) + XFree (status_attr); + + if (preedit_attr) + XFree (preedit_attr); + + DebugPrint ("created IC %p", input->xic); +} + +static void +IMDestroyCallback (XIM im, XPointer client_data, XPointer call_data) +{ + TextInputClientInfo *info; + TextInput *input; + + DebugPrint ("XIM %p destroyed", im); + + if (im != current_xim) + /* Is this even possible? */ + return; + + /* The XIM was destroyed, and all XICs have been freed. Clear all + fields still referencing XICs or XIMs. */ + + current_xim = NULL; + + /* Close the cd. */ + if (current_cd != (iconv_t) -1) + iconv_close (current_cd); + current_cd = (iconv_t) -1; + + /* Clear the XIC field of each input. */ + + info = all_client_infos.next; + while (info != &all_client_infos) + { + input = info->inputs.next; + while (input != &info->inputs) + { + /* Destroy the XIC of this one input. */ + if (input->xic) + { + input->xic = NULL; + + /* Handle IC destruction. */ + HandleICDestroyed (input); + } + + /* Move to the next input. */ + input = input->next; + } + + /* Move to the next client info. */ + info = info->next; + } + + DebugPrint ("finished XIM destruction"); +} + +static XIMStyle +CheckStyle (XIMStyles *styles, XIMStyle preedit_style, + XIMStyle status_style) +{ + int i; + + /* Is this preedit & status style combination supported? */ + for (i = 0; i < styles->count_styles; i++) + { + if ((styles->supported_styles[i] & preedit_style) + && (styles->supported_styles[i] & status_style)) + return styles->supported_styles[i]; + } + + return 0; +} + +static void +CheckStyles (XIM xim) +{ + XIMStyles *styles; + XIMStyle style; + int i; + + /* Pick a supported XIM style from the current input method. The + following input styles are supported: + + over-the-spot, where the preedit is displayed in a window at a + given position. + + off-the-spot, where the preedit is displayed in a window + somewhere inside the application window. + + root-window, where the preedit is displayed is displayed in a + window that is a child of the root window. + + on-the-spot, where the preedit is displayed inside the + application window. */ + + if (XGetIMValues (xim, XNQueryInputStyle, &styles, NULL)) + { + /* An error occured; default to none. */ + xim_style = XIMPreeditNone | XIMStatusNone; + return; + } + + /* Otherwise, find the best style in our order of preference. */ + for (i = 0; xim_style_order[i] != XimStyleNone; ++i) + { + DebugPrint ("considering style: %u", xim_style_order[i]); + + switch (xim_style_order[i]) + { + case XimOverTheSpot: + DebugPrint ("checking for over-the-spot"); + style = CheckStyle (styles, XIMPreeditPosition, + XIMStatusArea | XIMStatusNothing | XIMStatusNone); + if (style) + goto done; + break; + + case XimOffTheSpot: + DebugPrint ("checking for off-the-spot"); + style = CheckStyle (styles, XIMPreeditArea, + XIMStatusArea | XIMStatusNothing | XIMStatusNone); + if (style) + goto done; + break; + + case XimRootWindow: + DebugPrint ("checking for root-window"); + style = CheckStyle (styles, XIMPreeditNothing, + XIMStatusNothing | XIMStatusNone); + if (style) + goto done; + break; + + case XimOnTheSpot: + DebugPrint ("checking for on-the-spot"); + style = CheckStyle (styles, XIMPreeditCallbacks, + XIMStatusArea | XIMStatusNothing | XIMStatusNone); + if (style) + goto done; + break; + + case XimStyleNone: + /* This shouldn't happen. */ + abort (); + } + } + + DebugPrint ("checking for input method styles failed"); + /* No style could be found, so fall back to XIMPreeditNone and + XIMStatusNone. */ + style = XIMPreeditNone | XIMStatusNone; + done: + DebugPrint ("set styles to: %lu", (unsigned long) style); + XFree (styles); + xim_style = style; +} + +static void +HandleNewIM (XIM xim) +{ + TextInputClientInfo *info; + TextInput *input; + const char *locale; + char *oldlocale, *coding; + iconv_t cd; + XIMCallback destroy_callback; + + /* A new input method is available; destroy the XIC of every text + input. */ + info = all_client_infos.next; + while (info != &all_client_infos) + { + input = info->inputs.next; + while (input != &info->inputs) + { + /* Destroy the XIC of this one input. */ + if (input->xic) + { + XDestroyIC (input->xic); + input->xic = NULL; + + /* Handle IC destruction. */ + HandleICDestroyed (input); + } + + /* Move to the next input. */ + input = input->next; + } + + /* Move to the next client info. */ + info = info->next; + } + + /* Now, it is okay to delete the current XIM. */ + if (current_xim) + XCloseIM (current_xim); + current_xim = NULL; + + /* And its cd. */ + if (current_cd != (iconv_t) -1) + iconv_close (current_cd); + current_cd = (iconv_t) -1; + + /* Obtain the locale of the new input method. */ + locale = XLocaleOfIM (xim); + + /* Temporarily switch to the new locale to determine its coded + character set. */ + oldlocale = XLStrdup (setlocale (LC_ALL, NULL)); + + if (!setlocale (LC_ALL, locale)) + { + /* The locale specified by the input method couldn't be set. */ + XLFree (oldlocale); + goto bad_locale; + } + + /* Now we are in the input method locale. Obtain the codeset. */ + coding = XLStrdup (nl_langinfo (CODESET)); + + /* Switch back to the new locale. */ + if (!setlocale (LC_ALL, oldlocale)) + abort (); + + DebugPrint ("input method coding system is %s", coding); + + /* Create a character conversion context for input data. */ + cd = iconv_open ("UTF-8", coding); + + /* Free the new data. */ + XLFree (oldlocale); + XLFree (coding); + + /* If cd creation failed, assume it isn't supported. */ + if (cd == (iconv_t) -1) + goto bad_locale; + + DebugPrint ("conversion descriptor created to UTF-8"); + + /* Now enable the input method and create XICs for all text inputs. + Then, restore previous state. */ + current_xim = xim; + current_cd = cd; + + /* Attach the destroy callback to the XIM. */ + destroy_callback.client_data = NULL; + destroy_callback.callback = IMDestroyCallback; + XSetIMValues (xim, XNDestroyCallback, &destroy_callback, + NULL); + + /* Initialize the styles supported by this input method. */ + CheckStyles (xim); + + /* A new input method is available; destroy the XIC of every text + input. */ + info = all_client_infos.next; + while (info != &all_client_infos) + { + input = info->inputs.next; + while (input != &info->inputs) + { + /* Try to create the IC for this one input. */ + if (input->current_state.enabled) + { + CreateIC (input); + + /* Focus the IC and do geometry allocation. */ + if (input->xic) + XSetICFocus (input->xic); + DoGeometryAllocation (input); + } + + /* Move to the next input. */ + input = input->next; + } + + /* Move to the next client info. */ + info = info->next; + } + + return; + + bad_locale: + XCloseIM (xim); +} + +static void +IMInstantiateCallback (Display *display, XPointer client_data, + XPointer call_data) +{ + XIM newim; + + DebugPrint ("input method instantiated"); + + /* Open the input method. */ + newim = XOpenIM (compositor.display, + XrmGetDatabase (compositor.display), + (char *) compositor.resource_name, + (char *) compositor.app_name); + + /* Obtain its locale. */ + if (newim) + { + DebugPrint ("created input method with locale: %s", + XLocaleOfIM (newim)); + HandleNewIM (newim); + } + else + DebugPrint ("input method creation failed"); +} + +static void +FocusInCallback (Seat *seat, Surface *surface) +{ + TextInputClientInfo *info, *start; + + DebugPrint ("seat %p, surface %p", seat, surface); + + info = GetClientInfo (wl_resource_get_client (surface->resource), + seat, False); + + if (info) + { + DebugPrint ("found seat client info; sending events"); + NoticeEnter (info, surface); + } + + start = info ? info : &all_client_infos; + info = start->next; + + /* Now, leave all of the other infos on the same seat. */ + while (info != start) + { + if (info->seat == seat) + NoticeLeave (info); + + /* Note that info->seat will be NULL for the sentinel node, so + the above comparison can never be true. */ + info = info->next; + } +} + +static void +FocusOutCallback (Seat *seat) +{ + TextInputClientInfo *info; + + DebugPrint ("seat %p", seat); + + info = all_client_infos.next; + while (info != &all_client_infos) + { + /* Leave the info if this is the same seat. */ + if (info->seat == seat) + NoticeLeave (info); + + info = info->next; + } +} + +static void +ConvertKeyEvent (XIDeviceEvent *xev, XEvent *event) +{ + /* Input methods cannot understand extension events, so filter an + equivalent core event instead. */ + + memset (event, 0, sizeof *event); + + if (xev->evtype == XI_KeyPress) + event->xkey.type = KeyPress; + else + event->xkey.type = KeyRelease; + + event->xkey.serial = xev->serial; + event->xkey.send_event = xev->send_event; + event->xkey.display = compositor.display; + event->xkey.window = xev->event; + event->xkey.root = xev->root; + event->xkey.subwindow = xev->child; + event->xkey.time = xev->time; + event->xkey.state = ((xev->mods.effective & ~(1 << 13 | 1 << 14)) + | (xev->group.effective << 13)); + event->xkey.keycode = xev->detail; + event->xkey.x = xev->event_x; + event->xkey.y = xev->event_y; + event->xkey.x_root = xev->root_x; + event->xkey.y_root = xev->root_y; + + if (xev->root == DefaultRootWindow (compositor.display)) + event->xkey.same_screen = True; + + /* Wayland clients don't expect to receive repeated key events, + while input methods do. However, there is no way to stuff the + XIKeyRepeat flag into a core event. Our saving graces are that: + + - the high two bits of a valid XID are not set. + + - event->xkey.subwindow is unused by all input methods. + + - it cannot be valid to actually query information from the + subwindow, since it may no longer exist by the time the event + is forwarded to the input method. + + As a result, it becomes possible to record that information by + setting the high bit of the event subwindow for repeated key + events. */ + + if (xev->flags & XIKeyRepeat) + event->xkey.subwindow |= (1U << 31); +} + +static char * +ConvertString (char *buffer, size_t nbytes, size_t *size_out) +{ + char *outbuf, *outptr; + size_t outsize, outbytesleft, rc; + + outbuf = XLMalloc (BUFSIZ + 1); + outptr = outbuf; + outsize = BUFSIZ; + outbytesleft = outsize; + + DebugPrint ("converting string of size %tu", nbytes); + + /* Reset the cd state. */ + iconv (current_cd, NULL, NULL, &outptr, &outbytesleft); + + /* Start converting. */ + while (nbytes > 0) + { + rc = iconv (current_cd, &buffer, &nbytes, + &outptr, &outbytesleft); + + DebugPrint ("iconv gave: %tu", rc); + + if (rc == (size_t) -1) + { + /* See what went wrong. */ + if (errno == E2BIG) + { + /* Reallocate the output buffer. */ + outbuf = XLRealloc (outbuf, outsize + BUFSIZ + 1); + + /* Move the outptr to the right location in the new + outbuf. */ + outptr = outbuf + outsize - outbytesleft; + + /* Expand outsize and outbytesleft. */ + outsize += BUFSIZ; + outbytesleft += BUFSIZ; + + DebugPrint ("expanding outsize to %tu, outbytesleft now %tu", + outsize, outbytesleft); + } + else + goto finish; + } + } + + finish: + DebugPrint ("conversion finished, size_out %tu", + outsize - outbytesleft); + + /* Return outbuf and the number of bytes put in it. */ + if (size_out) + *size_out = outsize - outbytesleft; + + /* NULL-terminate the string. */ + outbuf[outsize - outbytesleft] = '\0'; + + return outbuf; +} + +static void +PreeditString (TextInput *input, const char *buffer, + size_t buffer_size, ptrdiff_t cursor) +{ + char chunk[4000]; + const char *start, *end; + int skip; + const char *buffer_end; + int cursor_pos; + + start = buffer; + buffer_end = buffer + buffer_size; + + /* The Wayland protocol limits strings to 4000 bytes (including the + terminating NULL). Send the text as valid substrings consisting + of less than 4000 bytes each. */ + + while (start < buffer_end) + { + end = start; + + while (true) + { + skip = CountOctets (*end); + + DebugPrint ("skip %d (%p+%d)", skip, end, skip); + + if (end + skip - start >= 3998) + break; + + if (end >= buffer_end) + break; + + end += skip; + } + + DebugPrint ("end-start (%p-%p): %zd", end, start, + end - start); + + /* Now, start to end contain a UTF-8 sequence less than 4000 + bytes in length. */ + XLAssert (end - start < 3998); + memcpy (chunk, start, end - start); + + /* NULL-terminate the buffer. */ + chunk[end - start] = '\0'; + DebugPrint ("sending buffered string %s", chunk); + + /* Calculate the cursor position and whether or not it is in + this chunk. */ + + if (cursor == -1) + cursor_pos = -1; + else + cursor_pos = cursor - (start - buffer); + + if (cursor_pos < 0) + cursor_pos = -1; + + /* Send the sequence. */ + zwp_text_input_v3_send_preedit_string (input->resource, chunk, + cursor_pos, cursor_pos); + + start = end; + } + + /* Finish sending it. */ + zwp_text_input_v3_send_done (input->resource, input->serial); +} + +static void +CommitString (TextInput *input, const char *buffer, + size_t buffer_size) +{ + char chunk[4000]; + const char *start, *end; + int skip; + const char *buffer_end; + + start = buffer; + buffer_end = buffer + buffer_size; + + /* The Wayland protocol limits strings to 4000 bytes (including the + terminating NULL). Send the text as valid substrings consisting + of less than 4000 bytes each. */ + + while (start < buffer_end) + { + end = start; + + while (true) + { + skip = CountOctets (*end); + + DebugPrint ("skip %d (%p+%d)", skip, end, skip); + + if (end + skip - start >= 3998) + break; + + if (end >= buffer_end) + break; + + end += skip; + } + + DebugPrint ("end-start (%p-%p): %zd", end, start, + end - start); + + /* Now, start to end contain a UTF-8 sequence less than 4000 + bytes in length. */ + XLAssert (end - start < 3998); + memcpy (chunk, start, end - start); + + /* NULL-terminate the buffer. */ + chunk[end - start] = '\0'; + DebugPrint ("sending buffered string %s", chunk); + + /* Send the sequence. */ + zwp_text_input_v3_send_commit_string (input->resource, chunk); + + start = end; + } + + /* Finish sending it. */ + zwp_text_input_v3_send_done (input->resource, input->serial); +} + +static Bool +LookupString (TextInput *input, XEvent *event, KeySym *keysym_return) +{ + char *buffer; + size_t nbytes, buffer_size; + Status status; + KeySym keysym; + + /* First, do XmbLookupString with the default buffer size. */ + buffer = alloca (256); + nbytes = XmbLookupString (input->xic, &event->xkey, + buffer, 256, &keysym, &status); + DebugPrint ("looked up %zu", nbytes); + + if (status == XBufferOverflow) + { + DebugPrint ("overflow to %zu", nbytes); + + /* Handle overflow by growing the buffer. */ + buffer = alloca (nbytes + 1); + nbytes = XmbLookupString (input->xic, &event->xkey, + buffer, nbytes + 1, + &keysym, &status); + } + + DebugPrint ("status is: %d", (int) status); + + /* If no string was returned, return False. Otherwise, convert the + string to UTF-8 and commit it. */ + if (status != XLookupChars && status != XLookupBoth) + { + if (status == XLookupKeySym && keysym_return) + /* Return the keysym if it was looked up. */ + *keysym_return = keysym; + + return False; + } + + DebugPrint ("converting buffer of %zu", nbytes); + + /* current_xim should not be NULL. */ + XLAssert (current_xim != NULL); + + /* Convert the string. */ + buffer = ConvertString (buffer, nbytes, &buffer_size); + + /* If the string happens to consist of only 1 character and a keysym + was also found, give preference to the keysym. */ + if (buffer_size == 1 && status == XLookupBoth) + { + DebugPrint ("using keysym in preference to single char"); + + XFree (buffer); + + if (keysym_return) + *keysym_return = keysym; + + return False; + } + + if (buffer) + CommitString (input, buffer, buffer_size); + XFree (buffer); + + return True; +} + +static Bool +FilterInputCallback (Seat *seat, Surface *surface, void *event) +{ + XIDeviceEvent *xev; + XEvent xkey; + TextInputClientInfo *info; + TextInput *input; + + xev = event; + + DebugPrint ("seat %p, surface %p, detail: %d, event: %lx", + seat, surface, xev->detail, xev->event); + + /* Find the client info. */ + info = GetClientInfo (wl_resource_get_client (surface->resource), + seat, False); + + /* Find the enabled text input. */ + if (info) + input = FindEnabledTextInput (info); + + /* If there is an enabled text input, start filtering the event. */ + if (info && input && input->xic) + { + DebugPrint ("found enabled text input %p on client-seat info %p", + input, info); + + /* Convert the extension event into a fake core event that the + input method can understand. */ + ConvertKeyEvent (xev, &xkey); + + /* And return the result of filtering the event. */ + if (XFilterEvent (&xkey, XLWindowFromSurface (surface))) + return True; + + /* Otherwise, call XmbLookupString. If a keysym is returned, + return False. Otherwise, commit the string looked up and + return True. */ + return LookupString (input, &xkey, NULL); + } + + /* Otherwise, do nothing. */ + return False; +} + + + +/* Seat input callbacks. */ +static TextInputFuncs input_funcs = + { + .focus_in = FocusInCallback, + .focus_out = FocusOutCallback, + .filter_input = FilterInputCallback, + }; + +void +XLTextInputDispatchCoreEvent (Surface *surface, XEvent *event) +{ + Seat *im_seat; + TextInputClientInfo *info; + TextInput *input; + KeySym keysym; + + DebugPrint ("dispatching core event to surface %p:\n" + "\ttype: %d\n" + "\tserial: %lu\n" + "\tsend_event: %d\n" + "\twindow: %lx\n" + "\troot: %lx\n" + "\tsubwindow: %lx\n" + "\ttime: %lu\n" + "\tstate: %x\n" + "\tkeycode: %x", surface, + event->xkey.type, + event->xkey.serial, event->xkey.send_event, + event->xkey.window, event->xkey.subwindow, + event->xkey.subwindow, event->xkey.time, + event->xkey.state, event->xkey.keycode); + + keysym = 0; + + /* Some of the events we want here are rather special. They are put + back on the event queue by the X internationalization library in + response to an XIM_COMMIT event being received. Other events are + put back on the event queue in response to XIM_FORWARD_EVENT. + First, find out which seat is the input method seat. */ + + im_seat = XLSeatGetInputMethodSeat (); + + if (!im_seat) + return; + + /* Next, find the client info associated with the surface for that + seat. */ + info = GetClientInfo (wl_resource_get_client (surface->resource), + im_seat, False); + + if (!info) + return; + + if (info->focus_surface != surface) + /* The surface is no longer focused. */ + return; + + if (info) + { + /* And look for an enabled text input. */ + input = FindEnabledTextInput (info); + + if (input) + { + DebugPrint ("found enabled input %p on info %p", input, info); + + /* Now, try to dispatch the core event. First, look up the + text string. */ + if (!input->xic || LookupString (input, event, &keysym)) + return; + + if (event->xkey.subwindow & (1U << 31)) + DebugPrint ("lookup failed; not dispatching event because" + " this is a key repeat"); + else + { + /* Since that failed, dispatch the event to the seat. */ + DebugPrint ("lookup failed; dispatching event to seat"); + + XLSeatDispatchCoreKeyEvent (im_seat, surface, event, keysym); + } + } + } +} + +static Bool +InitFontset (void) +{ + XrmDatabase rdb; + XrmName namelist[3]; + XrmClass classlist[3]; + XrmValue value; + XrmRepresentation type; + char **missing_charset_list, *def_string; + int missing_charset_count; + + rdb = XrmGetDatabase (compositor.display); + + if (!rdb) + return False; + + DebugPrint ("initializing fontset"); + + namelist[1] = XrmStringToQuark ("ximFont"); + namelist[0] = app_quark; + namelist[2] = NULLQUARK; + + classlist[1] = XrmStringToQuark ("XimFont"); + classlist[0] = resource_quark; + classlist[2] = NULLQUARK; + + if (XrmQGetResource (rdb, namelist, classlist, + &type, &value) + && type == QString) + { + DebugPrint ("XIM fontset: %s", value.addr); + + im_fontset = XCreateFontSet (compositor.display, + (char *) value.addr, + &missing_charset_list, + &missing_charset_count, + &def_string); + + if (missing_charset_count) + XFreeStringList (missing_charset_list); + return True; + } + + return False; +} + +static void +InitInputStyles (void) +{ + XrmDatabase rdb; + XrmName namelist[3]; + XrmClass classlist[3]; + XrmValue value; + XrmRepresentation type; + int i; + char *string, *end, *sep, *buffer; + + rdb = XrmGetDatabase (compositor.display); + + if (!rdb) + return; + + DebugPrint ("initializing input styles"); + + namelist[1] = XrmStringToQuark ("ximStyles"); + namelist[0] = app_quark; + namelist[2] = NULLQUARK; + + classlist[1] = XrmStringToQuark ("XimStyles"); + classlist[0] = resource_quark; + classlist[2] = NULLQUARK; + + if (XrmQGetResource (rdb, namelist, classlist, + &type, &value) + && type == QString) + { + DebugPrint ("XIM styles: %s", value.addr); + string = value.addr; + end = string + strlen (string); + i = 0; + + while (string < end) + { + /* Find the next comma. */ + sep = strchr (string, ','); + + if (!sep) + sep = end; + + /* Copy the text between string and sep into buffer. */ + buffer = alloca (sep - string + 1); + memcpy (buffer, string, sep - string); + buffer[sep - string] = '\0'; + + /* If the comparison is successful, populate the list. */ + DebugPrint ("considering: %s", buffer); + + if (!strcmp (buffer, "overTheSpot")) + xim_style_order[i++] = XimOverTheSpot; + else if (!strcmp (buffer, "offTheSpot")) + xim_style_order[i++] = XimOffTheSpot; + else if (!strcmp (buffer, "rootWindow")) + xim_style_order[i++] = XimRootWindow; + else if (!strcmp (buffer, "onTheSpot")) + xim_style_order[i++] = XimOnTheSpot; + else + { + /* Invalid value encountered; stop parsing. */ + DebugPrint ("invalid value: %s", buffer); + return; + } + + /* Return if i is now 4. */ + if (i == 4) + return; + + string = sep + 1; + } + + return; + } + else + { + /* Set up default values. */ + + xim_style_order[0] = XimOverTheSpot; + xim_style_order[1] = XimOffTheSpot; + xim_style_order[2] = XimRootWindow; + xim_style_order[3] = XimOnTheSpot; + + DebugPrint ("set up default values for XIM style order"); + } + + return; +} + +void +XLInitTextInput (void) +{ + const char *modifiers; + char **missing_charset_list, *def_string; + int missing_charset_count; + + current_cd = (iconv_t) -1; + + if (!XSupportsLocale ()) + { + DebugPrint ("not initializing text input because the" + " locale is not supported by the X library"); + return; + } + + /* Append the contents of XMODIFIERS to the locale modifiers + list. */ + modifiers = XSetLocaleModifiers (""); + DebugPrint ("locale modifiers are: %s", modifiers); + + /* Prevent -Wunused-but-set-variable when not debug. */ + ((void) modifiers); + + all_client_infos.next = &all_client_infos; + all_client_infos.last = &all_client_infos; + + text_input_manager_global + = wl_global_create (compositor.wl_display, + &zwp_text_input_manager_v3_interface, + 1, NULL, HandleBind); + + /* Initialize the IM fontset. */ + if (!InitFontset ()) + { + im_fontset = XCreateFontSet (compositor.display, + "-*-*-*-R-*-*-*-120-*-*-*-*", + &missing_charset_list, + &missing_charset_count, + &def_string); + if (missing_charset_count) + XFreeStringList (missing_charset_list); + } + + /* Initialize input styles. */ + InitInputStyles (); + + if (im_fontset == NULL) + fprintf (stderr, "Unable to load any usable fontset for input methods"); + + /* Register the IM callback. */ + XRegisterIMInstantiateCallback (compositor.display, + XrmGetDatabase (compositor.display), + (char *) compositor.resource_name, + (char *) compositor.app_name, + IMInstantiateCallback, NULL); + + /* Register the text input functions. */ + XLSeatSetTextInputFuncs (&input_funcs); +}