diff --git a/debian/control b/debian/control index 8b97a2b7..b4d16a57 100644 --- a/debian/control +++ b/debian/control @@ -10,6 +10,7 @@ Build-Depends: libgtk-3-dev, libcroco3-dev, libwayland-dev (>= 1.16), + rustc, wayland-protocols (>= 1.14) Standards-Version: 4.1.3 Homepage: https://source.puri.sm/Librem5/squeekboard diff --git a/eekboard/eekboard-context-service.h b/eekboard/eekboard-context-service.h index 3fb5d237..c56cb4e0 100644 --- a/eekboard/eekboard-context-service.h +++ b/eekboard/eekboard-context-service.h @@ -24,6 +24,8 @@ #include +#include "virtual-keyboard-unstable-v1-client-protocol.h" + G_BEGIN_DECLS #define EEKBOARD_CONTEXT_SERVICE_PATH "/org/fedorahosted/Eekboard/Context_%d" diff --git a/meson.build b/meson.build index 70893d29..38e6db1e 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project( 'squeekboard', - 'c', + 'c', 'rust', version: '1.0.9', license: 'GPLv3', meson_version: '>=0.43.0', diff --git a/protocols/input-method-unstable-v2.xml b/protocols/input-method-unstable-v2.xml new file mode 100644 index 00000000..97484d81 --- /dev/null +++ b/protocols/input-method-unstable-v2.xml @@ -0,0 +1,404 @@ + + + + + Copyright © 2012, 2013 Intel Corporation + Copyright © 2015, 2016 Jan Arne Petersen + Copyright © 2017, 2018 Red Hat, Inc. + Copyright © 2018 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows applications to act as input methods for compositors. + + An input method context is used to manage the state of the input method. + + Text strings are UTF-8 encoded, their indices and lengths are in bytes. + + 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. + + + + + An input method object allows for clients to compose text. + + The objects connects the client to a text input in an application, and + lets the client to serve as an input method for a seat. + + The zwp_input_method_v2 object can occupy two distinct states: active and + inactive. In the active state, the object is associated to and + communicates with a text input. In the inactive state, there is no + associated text input, and the only communication is with the compositor. + Initially, the input method is in the inactive state. + + Requests issued in the inactive state must be accepted by the compositor. + Because of the serial mechanism, and the state reset on activate event, + they will not have any effect on the state of the next text input. + + There must be no more than one input method object per seat. + + + + + Notification that a text input focused on this seat requested the input + method to be activated. + + This request must be issued every time a text input requests an input + method. + + 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. In + addition, it marks the input method object as active. + + The set_surrounding_text, set_content_type and set_cursor_rectangle + requests must follow before the next done event if the text input + supports the respective functionality. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Notification that this seat's current text input requested the input + method to be deactivated. + + This event mrks the zwp_input_method_v2 object as inactive. + + When the seat has the keyboard capability the text-input focus follows + the keyboard focus. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Sets the surrounding plain text around the cursor, excluding the + preedit text. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. + + The argument text is a buffer containing the preedit string, and must + include the cursor position, and the complete selection. It should + contain additional characters before and after these. 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 the text buffer. + + anchor is the byte offset of the selection anchor within the text + buffer. If there is no selected text, anchor must be the same as + cursor. + + If this request does not arrive before the first done event, the input + method may assume that the text input does not support this + functionality and ignore following surrounding_text events. + + Values set with this event are double-buffered. They will get applied + and set to initial values on the next zwp_input_method_v2.done + event. + + 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. + + + + + + + + + Tells the input method why the text surrounding the cursor changed. + + Whenever the client detects an external change in text, cursor, or + anchor position, 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 event is double-buffered. It will get applied + and set to its initial value on the next zwp_input_method_v2.done + event. + + The initial value of cause is input_method. + + + + + + + Indicates the content type and hint for the current + input_method_context instance. + + Values set with this event are double-buffered. They will get applied + on the next zwp_input_method_v2.done event. + + The initial value for hint is none, and the initial value for purpose + is normal. + + + + + + + + Atomically applies state changes recently sent to the client. + + The done event establishes and updates the state of the client, and + must be issued after any changes to apply them. + + Text input state (content purpose, content hint, surrounding text, and + change cause) is conceptually double-buffered within an input method + context. + + Events modify the pending state, as opposed to the current state in use + by the input method. A done event atomically applies all pending state, + replacing the current state. After done, the new pending state is as + documented for each related request. + + Events must be applied in the order of arrival. + + Neither current nor pending state are modified unless noted otherwise. + + + + + + Send the commit string text for insertion to the application. + + Inserts a string at current cursor position (see commit event + sequence). The string to commit could be either just a single character + after a key press or the result of some composing. + + The argument text is a buffer containing the string to insert. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + 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. + + + + + + + Send the pre-edit string text to the application text input. + + Place a new composing text (pre-edit) at the current cursor position. + Any previously set composing text must be removed. Any previously + existing selected text must be removed. The cursor is moved to a new + position within the preedit string. + + The argument text is a buffer containing the preedit string. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + The arguments cursor_begin and cursor_end are counted in bytes relative + to the beginning of the submitted string buffer. Cursor should be + hidden by the text input when both are equal to -1. + + cursor_begin indicates the beginning of the cursor. cursor_end + indicates the end of the cursor. It may be equal or different than + cursor_begin. + + Values set with this event are double-buffered. They must be applied on + the next zwp_input_method_v2.commit event. + + The initial value of text is an empty string. The initial value of + cursor_begin, and cursor_end are both 0. + + + + + + + + + Remove the surrounding text. + + before_length and after_length are the number of bytes before and after + the current cursor index (excluding the preedit text) to delete. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. In effect before_length is counted from the + beginning of preedit text, and after_length from its end (see commit + event sequence). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_input_method_v2.commit request. + + The initial values of both before_length and after_length are 0. + + + + + + + + Apply state changes from commit_string, preedit_string and + delete_surrounding_text requests. + + The state relating to these events is double-buffered, and each one + modifies the pending state. This request replaces the current state + with the pending state. + + The connected text input is expected to 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_input_method_v2 + object known to the client. The value of the serial argument must be + equal to the number of commit requests already issued on that object. + When the compositor receives a done event with a serial different than + the number of past commit requests, it must proceed as normal, except + it should not change the current state of the zwp_input_method_v2 + object. + + + + + + + Creates a new zwp_input_popup_surface_v2 object wrapping a given + surface. + + + + + + + + Allow an input method to receive hardware keyboard input and process + key events to generate text events (with pre-edit) over the wire. This + allows input methods which compose multiple key events for inputting + text like it is done for CJK languages. + + The compositor should send all keyboard events on the seat to the grab + holder via the returned wl_keyboard object. Nevertheless, the + compositor may decide not to forward any particular event. The + compositor must not further process any event after it has been + forwarded to the grab holder. + + Releasing the resulting wl_keyboard object releases the grab. + + + + + + + The input method ceased to be available. + + The compositor must issue this event as the only event on the object if + there was another input_method object associated with the same seat at + the time of its creation. + + The compositor must issue this request when the object is no longer + useable, e.g. due to seat removal. + + The input method context becomes inert and should be destroyed after + deactivation is handled. Any further requests and events except for the + destroy request must be ignored. + + + + + + + + + This surface is a popup for interacting with an input method. + + The compositor should place it near the active text input area. It must + be visible if and only if the input method is in the active state. + + + + + Notify about the position of the area of the text input expressed as a + rectangle in surface local coordinates. + + This is a hint to the input method telling it the relative position of + the text being entered. + + + + + + + + + + + + + The input method manager allows the client to become the input method on + a chosen seat. + + No more than one input method must be associated with any seat at any + given time. + + + + + Request a new input zwp_input_method_v2 object associated with a given + seat. + + + + + + + + Destroys the zwp_input_method_manager_v2 object. + + The zwp_input_method_v2 objects originating from it remain valid. + + + + diff --git a/protocols/meson.build b/protocols/meson.build index 57c9e8a7..b989eb7b 100644 --- a/protocols/meson.build +++ b/protocols/meson.build @@ -13,6 +13,7 @@ wl_protos = [ wl_protocol_dir + '/stable/xdg-shell/xdg-shell.xml', 'wlr-layer-shell-unstable-v1.xml', 'virtual-keyboard-unstable-v1.xml', + 'input-method-unstable-v2.xml', ] wl_proto_sources = [] foreach proto: wl_protos diff --git a/src/imservice.c b/src/imservice.c new file mode 100644 index 00000000..aed73222 --- /dev/null +++ b/src/imservice.c @@ -0,0 +1,43 @@ +#include "imservice.h" + +#include + +#include "eekboard/eekboard-context-service.h" + + +void imservice_handle_text_change_cause(void *data, struct zwp_input_method_v2 *input_method) {} +void imservice_handle_content_type(void *data, struct zwp_input_method_v2 *input_method) {} +void imservice_handle_unavailable(void *data, struct zwp_input_method_v2 *input_method) {} + + +static const struct zwp_input_method_v2_listener input_method_listener = { + .activate = imservice_handle_input_method_activate, + .deactivate = imservice_handle_input_method_deactivate, + .surrounding_text = imservice_handle_surrounding_text, + .text_change_cause = imservice_handle_text_change_cause, + .content_type = imservice_handle_content_type, + .done = imservice_handle_commit_state, + .unavailable = imservice_handle_unavailable, +}; + +struct imservice* get_imservice(EekboardContextService *context, + struct zwp_input_method_manager_v2 *manager, + struct wl_seat *seat) { + struct zwp_input_method_v2 *im = zwp_input_method_manager_v2_get_input_method(manager, seat); + struct imservice *imservice = imservice_new(im, context); + zwp_input_method_v2_add_listener(im, + &input_method_listener, imservice); + return imservice; +} + +void imservice_make_visible(EekboardContextService *context, + struct zwp_input_method_v2 *im) { + (void)im; + eekboard_context_service_show_keyboard (context); +} + +void imservice_try_hide(EekboardContextService *context, + struct zwp_input_method_v2 *im) { + (void)im; + eekboard_context_service_hide_keyboard (context); +} diff --git a/src/imservice.h b/src/imservice.h new file mode 100644 index 00000000..b728401f --- /dev/null +++ b/src/imservice.h @@ -0,0 +1,22 @@ +#ifndef __IMSERVICE_H +#define __IMSERVICE_H + +#include "input-method-unstable-v2-client-protocol.h" +#include "eek/eek-types.h" + +struct imservice; + +struct imservice* get_imservice(EekboardContextService *context, + struct zwp_input_method_manager_v2 *manager, + struct wl_seat *seat); + +// Defined in Rust +struct imservice* imservice_new(struct zwp_input_method_v2 *im, + EekboardContextService *context); +void imservice_handle_input_method_activate(void *data, struct zwp_input_method_v2 *input_method); +void imservice_handle_input_method_deactivate(void *data, struct zwp_input_method_v2 *input_method); +void imservice_handle_surrounding_text(void *data, struct zwp_input_method_v2 *input_method, + const char *text, uint32_t cursor, uint32_t anchor); +void imservice_handle_commit_state(void *data, struct zwp_input_method_v2 *input_method); + +#endif diff --git a/src/imservice.rs b/src/imservice.rs new file mode 100644 index 00000000..8de61063 --- /dev/null +++ b/src/imservice.rs @@ -0,0 +1,137 @@ +use std::boxed::Box; +use std::ffi::CString; +use std::num::Wrapping; +use std::string::String; + + +/// Gathers stuff defined in C or called by C +pub mod c { + use super::*; + + use std::ffi::CStr; + use std::os::raw::{c_char, c_void}; + + fn into_cstring(s: *const c_char) -> Result { + CString::new( + unsafe {CStr::from_ptr(s)}.to_bytes() + ) + } + + // The following defined in C + + /// struct zwp_input_method_v2* + #[repr(transparent)] + pub struct InputMethod(*const c_void); + + /// EekboardContextService* + #[repr(transparent)] + pub struct UIManager(*const c_void); + + #[no_mangle] + extern "C" { + fn imservice_make_visible(imservice: *const UIManager); + fn imservice_try_hide(imservice: *const UIManager); + } + + // The following defined in Rust. TODO: wrap naked pointers to Rust data inside RefCells to prevent multiple writers + + #[no_mangle] + pub unsafe extern "C" + fn imservice_new(im: *const InputMethod, ui_manager: *const UIManager) -> *mut IMService { + Box::::into_raw(Box::new( + IMService { + im: im, + ui_manager: ui_manager, + pending: IMProtocolState::default(), + current: IMProtocolState::default(), + preedit_string: String::new(), + serial: Wrapping(0u32), + } + )) + } + + // TODO: is unsafe needed here? + #[no_mangle] + pub unsafe extern "C" + fn imservice_handle_input_method_activate(imservice: *mut IMService, + _im: *const InputMethod) + { + let imservice = &mut *imservice; + imservice.preedit_string = String::new(); + imservice.pending = IMProtocolState { + active: true, + ..IMProtocolState::default() + }; + } + + #[no_mangle] + pub unsafe extern "C" + fn imservice_handle_input_method_deactivate(imservice: *mut IMService, + _im: *const InputMethod) + { + let imservice = &mut *imservice; + imservice.pending = IMProtocolState { + active: false, + ..imservice.pending.clone() + }; + } + + #[no_mangle] + pub unsafe extern "C" + fn imservice_handle_surrounding_text(imservice: *mut IMService, + _im: *const InputMethod, + text: *const c_char, cursor: u32, _anchor: u32) + { + let imservice = &mut *imservice; + imservice.pending = IMProtocolState { + surrounding_text: into_cstring(text).expect("Received invalid string"), + surrounding_cursor: cursor, + ..imservice.pending + }; + } + + #[no_mangle] + pub unsafe extern "C" + fn imservice_handle_commit_state(imservice: *mut IMService, + _im: *const InputMethod) + { + let imservice = &mut *imservice; + let active_changed = imservice.current.active ^ imservice.pending.active; + + imservice.serial += Wrapping(1u32); + imservice.current = imservice.pending.clone(); + imservice.pending = IMProtocolState { + active: imservice.current.active, + ..IMProtocolState::default() + }; + if active_changed { + if imservice.current.active { + imservice_make_visible(imservice.ui_manager); + } else { + imservice_try_hide(imservice.ui_manager); + } + } + } + + // FIXME: destroy and deallocate +} + +/// Describes the desired state of the input method as requested by the server +#[derive(Default, Clone)] +struct IMProtocolState { + surrounding_text: CString, + surrounding_cursor: u32, + active: bool, +} + +pub struct IMService { + /// Owned reference (still created and destroyed in C) + im: *const c::InputMethod, + /// Unowned reference. Be careful, it's shared with C at large + ui_manager: *const c::UIManager, + + pending: IMProtocolState, + current: IMProtocolState, // turn current into an idiomatic representation? + preedit_string: String, + serial: Wrapping, +} diff --git a/src/meson.build b/src/meson.build index 787edc94..d253a54c 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,6 +5,7 @@ dbus_src = gnome.gdbus_codegen( ) sources = [ + 'imservice.c', 'server-context-service.c', 'server-main.c', 'wayland.c', @@ -61,6 +62,7 @@ deps = [ # Replacement for eekboard-server squeekboard = executable('squeekboard', sources, + link_with: library('rslib', 'imservice.rs'), include_directories: [include_directories('..'), include_directories('../eek')], dependencies: deps, install: true, diff --git a/src/server-main.c b/src/server-main.c index ca7ee1c1..84a2a280 100644 --- a/src/server-main.c +++ b/src/server-main.c @@ -33,6 +33,7 @@ #include "eekboard/eekboard-service.h" #include "eek/eek.h" +#include "imservice.h" #include "server-context-service.h" #include "wayland.h" @@ -43,6 +44,7 @@ struct squeekboard { struct squeek_wayland wayland; EekboardContextService *context; + struct imservice *imservice; }; @@ -114,6 +116,9 @@ registry_handle_global (void *data, } else if (!strcmp (interface, zwp_virtual_keyboard_manager_v1_interface.name)) { instance->wayland.virtual_keyboard_manager = wl_registry_bind(registry, name, &zwp_virtual_keyboard_manager_v1_interface, 1); + } else if (!strcmp (interface, zwp_input_method_manager_v2_interface.name)) { + instance->wayland.input_method_manager = wl_registry_bind(registry, name, + &zwp_input_method_manager_v2_interface, 1); } else if (!strcmp (interface, "wl_output")) { struct wl_output *output = wl_registry_bind (registry, name, &wl_output_interface, 2); @@ -244,6 +249,18 @@ main (int argc, char **argv) exit (1); } + struct imservice *imservice = NULL; + if (instance.wayland.input_method_manager) { + imservice = get_imservice(instance.context, + instance.wayland.input_method_manager, + instance.wayland.seat); + if (imservice) { + instance.imservice = imservice; + } else { + g_warning("Failed to register as an input method"); + } + } + GMainLoop *loop = g_main_loop_new (NULL, FALSE); g_signal_connect (service, "destroyed", G_CALLBACK(on_destroyed), loop); diff --git a/src/wayland.h b/src/wayland.h index 16f463e8..3907cc35 100644 --- a/src/wayland.h +++ b/src/wayland.h @@ -3,6 +3,7 @@ #include "wlr-layer-shell-unstable-v1-client-protocol.h" #include "virtual-keyboard-unstable-v1-client-protocol.h" +#include "input-method-unstable-v2-client-protocol.h" #include @@ -10,6 +11,7 @@ struct squeek_wayland { struct zwlr_layer_shell_v1 *layer_shell; struct zwp_virtual_keyboard_manager_v1 *virtual_keyboard_manager; + struct zwp_input_method_manager_v2 *input_method_manager; GPtrArray *outputs; // *wl_output struct wl_seat *seat; };