diff --git a/proto/protocol.xml b/proto/protocol.xml index 7266aa4..7b8a77a 100644 --- a/proto/protocol.xml +++ b/proto/protocol.xml @@ -781,6 +781,7 @@ - "ei_button" - "ei_keyboard" - "ei_touchscreen" + - "ei_text" The interface version is equal or less to the client-supported version in ei_handshake.interface_version for the respective interface. @@ -1584,4 +1585,109 @@ + + + + Interface for text-based requests and events. + + This interface is only provided once per device and where a client + requests ei_text.release the interface does not get re-initialized. An + EIS implementation may adjust the behavior of the device (including removing + the device) if the interface is released. + + Note that for a client to receive objects of this type, it must announce + support for this interface in ei_handshake.interface_version. + + + + + + + Notification that the client is no longer interested in this text interface object. + The EIS implementation will release any resources related to this object and + send the ei_text.destroyed event once complete. + + + + + + Generate an XKB key sym event. + + It is a client bug to send more than one key request for the same keysym + within the same ei_device.frame and the EIS implementation + may ignore either or all keysym state changes and/or disconnect the client. + + It is a protocol violation to send this request for a client + of an ei_handshake.context_type other than sender. + + It is a protocol violation to send this request in the same frame + as a ei_keyboard.key. + + + + + + + + Generate a UTF-8 compatible text string. + + It is a protocol violation to send this request for a client + of an ei_handshake.context_type other than sender. + + It is a protocol violation to send more than one utf8 request in the same + frame. + + The order of ei_keyboard.key or ei_text.keysym and ei_text.utf8 if sent + within the same frame is undefined. + + + + + + + + + This text interface object has been removed and a client should release all + associated resources. + + This ei_text object will be destroyed by the EIS implementation immediately after + after this event is sent and as such the client must not attempt to use + it after that point. + + + + + + + See the ei_text.keysym request for details. + + It is a protocol violation to send this request for a client + of an ei_handshake.context_type other than receiver. + + It is a protocol violation to send a keysym down event in the same + frame as a key up event for the same keysym in the same frame. + + It is a protocol violation to send this event in the same frame + as a ei_keyboard.key event. + + + + + + + + See the ei_text.utf8 request for details. + + It is a protocol violation to send this request for a client + of an ei_handshake.context_type other than receiver. + + It is a protocol violation to send more than one utf8 event in the same + frame. + + The order of ei_keyboard.key, ei_text.keysym, and ei_text.utf8 if sent + within the same frame is undefined. + + + + diff --git a/src/libei-device.c b/src/libei-device.c index 607160b..718fdc2 100644 --- a/src/libei-device.c +++ b/src/libei-device.c @@ -90,6 +90,7 @@ ei_device_destroy(struct ei_device *device) ei_button_unref(device->button); ei_touchscreen_unref(device->touchscreen); ei_keyboard_unref(device->keyboard); + ei_text_unref(device->text); ei_seat_unref(seat); free(device->name); free(device->pending_region_mapping_id); @@ -255,13 +256,16 @@ handle_msg_done(struct ei_device *device) mask_add(device->capabilities, EI_DEVICE_CAP_KEYBOARD); if (device->touchscreen) mask_add(device->capabilities, EI_DEVICE_CAP_TOUCH); + if (device->text) + mask_add(device->capabilities, EI_DEVICE_CAP_TEXT); if (!ei_device_has_capability(device, EI_DEVICE_CAP_POINTER) && !ei_device_has_capability(device, EI_DEVICE_CAP_POINTER_ABSOLUTE) && !ei_device_has_capability(device, EI_DEVICE_CAP_KEYBOARD) && !ei_device_has_capability(device, EI_DEVICE_CAP_TOUCH) && !ei_device_has_capability(device, EI_DEVICE_CAP_BUTTON) && - !ei_device_has_capability(device, EI_DEVICE_CAP_SCROLL)) { + !ei_device_has_capability(device, EI_DEVICE_CAP_SCROLL) && + !ei_device_has_capability(device, EI_DEVICE_CAP_TEXT)) { log_debug(ei, "Rejecting device %#" PRIx64 " '%s' with no known capabilities", ei_device_get_id(device), ei_device_get_name(device)); ei_device_close(device); @@ -273,7 +277,7 @@ handle_msg_done(struct ei_device *device) ei_queue_device_added_event(device); ei_device_done(device); log_debug(ei, - "Added device %#" PRIx64 " '%s' caps: %s%s%s%s%s%s seat: %s", + "Added device %#" PRIx64 " '%s' caps: %s%s%s%s%s%s%s seat: %s", ei_device_get_id(device), ei_device_get_name(device), ei_device_has_capability(device, EI_DEVICE_CAP_POINTER) ? "p" : "", ei_device_has_capability(device, EI_DEVICE_CAP_POINTER_ABSOLUTE) ? "a" : "", @@ -281,6 +285,7 @@ handle_msg_done(struct ei_device *device) ei_device_has_capability(device, EI_DEVICE_CAP_TOUCH) ? "t" : "", ei_device_has_capability(device, EI_DEVICE_CAP_BUTTON) ? "b" : "", ei_device_has_capability(device, EI_DEVICE_CAP_SCROLL) ? "s" : "", + ei_device_has_capability(device, EI_DEVICE_CAP_TEXT) ? "x" : "", ei_seat_get_name(ei_device_get_seat(device))); return NULL; } @@ -466,6 +471,12 @@ handle_msg_interface(struct ei_device *device, object_id_t id, const char *name, "Duplicate ei_touchscreen interface object on device"); device->touchscreen = ei_touchscreen_new(device, id, version); + } else if (streq(name, EI_TEXT_INTERFACE_NAME)) { + DISCONNECT_IF_INVALID_VERSION(ei, ei_text, id, version); + if (device->text) + return brei_result_new(EI_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "Duplicate ei_text interface object on device"); + device->text = ei_text_new(device, id, version); } else { return brei_result_new(EI_CONNECTION_DISCONNECT_REASON_PROTOCOL, "Unsupported interface '%s' on device", name); @@ -875,7 +886,8 @@ handle_msg_touch_cancel(struct ei_touchscreen *touchscreen, uint32_t touchid) } struct ei *ei = ei_device_get_context(device); - if (ei->interface_versions.ei_touchscreen < EI_TOUCHSCREEN_EVENT_CANCEL_SINCE_VERSION) { + if (ei->interface_versions.ei_touchscreen < + EI_TOUCHSCREEN_EVENT_CANCEL_SINCE_VERSION) { return brei_result_new(EI_CONNECTION_DISCONNECT_REASON_PROTOCOL, "Touch cancel event for touchscreen version v1"); } @@ -913,6 +925,75 @@ ei_device_get_touchscreen_interface(struct ei_device *device) return &touchscreen_interface; } +static struct brei_result * +handle_msg_text_destroy(struct ei_text *text, uint32_t serial) +{ + struct ei *ei = ei_text_get_context(text); + ei_update_serial(ei, serial); + + struct ei_device *device = ei_text_get_device(text); + ei_text_unref(steal(&device->text)); + + return NULL; +} + +static struct brei_result * +handle_msg_text_keysym(struct ei_text *text, uint32_t keysym, uint32_t state) +{ + struct ei_device *device = ei_text_get_device(text); + + DISCONNECT_IF_SENDER_CONTEXT(device); + + if (!ei_device_has_capability(device, EI_DEVICE_CAP_TEXT)) { + return brei_result_new(EI_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "keysym event for non-text device"); + } + + if (device->state == EI_DEVICE_STATE_EMULATING) { + ei_queue_text_keysym_event(device, keysym, !!state); + return NULL; + } + + return maybe_error_on_device_state(device, "text keysym"); +} + +static struct brei_result * +handle_msg_text_utf8(struct ei_text *text, const char *utf8) +{ + struct ei_device *device = ei_text_get_device(text); + + DISCONNECT_IF_SENDER_CONTEXT(device); + + if (!ei_device_has_capability(device, EI_DEVICE_CAP_TEXT)) { + return brei_result_new(EI_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "utf8 event for non-text device"); + } + + if (!utf8) { + return brei_result_new(EI_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "utf8 text is NULL"); + } + + if (device->state == EI_DEVICE_STATE_EMULATING) { + ei_queue_text_utf8_event(device, utf8); + return NULL; + } + + return maybe_error_on_device_state(device, "text utf8"); +} + +static const struct ei_text_interface text_interface = { + .destroyed = handle_msg_text_destroy, + .keysym = handle_msg_text_keysym, + .utf8 = handle_msg_text_utf8, +}; + +const struct ei_text_interface * +ei_device_get_text_interface(struct ei_device *device) +{ + return &text_interface; +} + struct ei_device * ei_device_new(struct ei_seat *seat, object_id_t deviceid, uint32_t version) { @@ -1194,6 +1275,7 @@ ei_device_has_capability(struct ei_device *device, case EI_DEVICE_CAP_TOUCH: case EI_DEVICE_CAP_BUTTON: case EI_DEVICE_CAP_SCROLL: + case EI_DEVICE_CAP_TEXT: return mask_all(device->capabilities, cap); } return false; @@ -1812,6 +1894,89 @@ ei_touch_cancel(struct ei_touch *touch) ei_send_touch_up(touch->device, touch->tracking_id); } +static int +ei_send_text_keysym(struct ei_device *device, uint32_t keysym, bool is_press) +{ + struct ei *ei = ei_device_get_context(device); + + if (ei->state == EI_STATE_NEW || ei->state == EI_STATE_DISCONNECTED) + return 0; + + device->send_frame_event = true; + + int rc = ei_text_request_keysym(device->text, keysym, is_press); + if (rc) + ei_disconnect(ei); + return rc; +} + +static int +ei_send_text_utf8(struct ei_device *device, const char *utf8) +{ + struct ei *ei = ei_device_get_context(device); + + if (ei->state == EI_STATE_NEW || ei->state == EI_STATE_DISCONNECTED) + return 0; + + device->send_frame_event = true; + + int rc = ei_text_request_utf8(device->text, utf8); + if (rc) + ei_disconnect(ei); + return rc; +} + +_public_ void +ei_device_text_keysym(struct ei_device *device, + uint32_t keysym, bool is_press) +{ + if (!ei_device_has_capability(device, EI_DEVICE_CAP_TEXT)) { + log_bug_client(ei_device_get_context(device), + "%s: device is not a text device", __func__); + return; + } + + if (device->state != EI_DEVICE_STATE_EMULATING) { + log_bug_client(ei_device_get_context(device), + "%s: device is not emulating", __func__); + return; + } + + ei_send_text_keysym(device, keysym, is_press); +} + +_public_ void +ei_device_text_utf8(struct ei_device *device, const char *utf8) +{ + ei_device_text_utf8_with_length(device, utf8, utf8 ? strlen(utf8) : 0); +} + +_public_ void +ei_device_text_utf8_with_length(struct ei_device *device, const char *utf8, size_t length) +{ + if (!ei_device_has_capability(device, EI_DEVICE_CAP_TEXT)) { + log_bug_client(ei_device_get_context(device), + "%s: device is not a text device", __func__); + return; + } + + if (device->state != EI_DEVICE_STATE_EMULATING) { + log_bug_client(ei_device_get_context(device), + "%s: device is not emulating", __func__); + return; + } + + if (length == 0) { + log_bug_client(ei_device_get_context(device), + "%s: empty utf8 string", __func__); + return; + } + + char buf[4096] = {0}; + memcpy(buf, utf8, min(length, sizeof(buf) - 1)); + ei_send_text_utf8(device, buf); +} + _public_ void ei_device_frame(struct ei_device *device, uint64_t time) { diff --git a/src/libei-device.h b/src/libei-device.h index 0679a12..2a91b4d 100644 --- a/src/libei-device.h +++ b/src/libei-device.h @@ -31,6 +31,7 @@ #include "brei-shared.h" #include "libei-pointer.h" #include "libei-keyboard.h" +#include "libei-text.h" #include "libei-touchscreen.h" enum ei_device_state { @@ -65,6 +66,7 @@ struct ei_device { struct ei_button *button; struct ei_keyboard *keyboard; struct ei_touchscreen *touchscreen; + struct ei_text *text; struct list link; enum ei_device_state state; @@ -123,6 +125,7 @@ OBJECT_DECLARE_GETTER(ei_device, scroll_interface, const struct ei_scroll_interf OBJECT_DECLARE_GETTER(ei_device, button_interface, const struct ei_button_interface *); OBJECT_DECLARE_GETTER(ei_device, keyboard_interface, const struct ei_keyboard_interface *); OBJECT_DECLARE_GETTER(ei_device, touchscreen_interface, const struct ei_touchscreen_interface *); +OBJECT_DECLARE_GETTER(ei_device, text_interface, const struct ei_text_interface *); OBJECT_DECLARE_SETTER(ei_device, type, enum ei_device_type); OBJECT_DECLARE_SETTER(ei_device, name, const char*); OBJECT_DECLARE_SETTER(ei_device, seat, const char*); diff --git a/src/libei-event.c b/src/libei-event.c index c3cc53d..5feb9c1 100644 --- a/src/libei-event.c +++ b/src/libei-event.c @@ -65,6 +65,8 @@ ei_event_type_to_string(enum ei_event_type type) CASE_RETURN_STRING(EI_EVENT_TOUCH_DOWN); CASE_RETURN_STRING(EI_EVENT_TOUCH_UP); CASE_RETURN_STRING(EI_EVENT_TOUCH_MOTION); + CASE_RETURN_STRING(EI_EVENT_TEXT_KEYSYM); + CASE_RETURN_STRING(EI_EVENT_TEXT_UTF8); } return NULL; @@ -98,6 +100,10 @@ ei_event_destroy(struct ei_event *event) case EI_EVENT_TOUCH_DOWN: case EI_EVENT_TOUCH_UP: case EI_EVENT_TOUCH_MOTION: + case EI_EVENT_TEXT_KEYSYM: + break; + case EI_EVENT_TEXT_UTF8: + free(steal(&event->text.utf8)); break; case EI_EVENT_DEVICE_ADDED: if (ei->interface_versions.ei_device >= EI_DEVICE_REQUEST_READY_SINCE_VERSION) @@ -377,6 +383,30 @@ ei_event_touch_get_is_cancel(struct ei_event *event) return event->touch.is_cancel; } +_public_ uint32_t +ei_event_text_get_keysym(struct ei_event *event) +{ + require_event_type(event, 0, EI_EVENT_TEXT_KEYSYM); + + return event->text.keysym; +} + +_public_ bool +ei_event_text_get_keysym_is_press(struct ei_event *event) +{ + require_event_type(event, false, EI_EVENT_TEXT_KEYSYM); + + return event->text.is_press; +} + +_public_ const char * +ei_event_text_get_utf8(struct ei_event *event) +{ + require_event_type(event, NULL, EI_EVENT_TEXT_UTF8); + + return event->text.utf8; +} + _public_ uint64_t ei_event_get_time(struct ei_event *event) { @@ -392,6 +422,8 @@ ei_event_get_time(struct ei_event *event) EI_EVENT_TOUCH_DOWN, EI_EVENT_TOUCH_UP, EI_EVENT_TOUCH_MOTION, + EI_EVENT_TEXT_KEYSYM, + EI_EVENT_TEXT_UTF8, EI_EVENT_FRAME); return event->timestamp; diff --git a/src/libei-event.h b/src/libei-event.h index 8f011f7..0c91c25 100644 --- a/src/libei-event.h +++ b/src/libei-event.h @@ -66,6 +66,11 @@ struct ei_event { double x, y; bool is_cancel; } touch; + struct { + uint32_t keysym; + bool is_press; + char *utf8; + } text; struct { uint32_t sequence; } start_emulating; diff --git a/src/libei-handshake.c b/src/libei-handshake.c index f157690..4fd5598 100644 --- a/src/libei-handshake.c +++ b/src/libei-handshake.c @@ -98,6 +98,7 @@ ei_handshake_initialize(struct ei_handshake *setup, uint32_t version) ei_handshake_request_interface_version(setup, EI_BUTTON_INTERFACE_NAME, v->ei_button); ei_handshake_request_interface_version(setup, EI_KEYBOARD_INTERFACE_NAME, v->ei_keyboard); ei_handshake_request_interface_version(setup, EI_TOUCHSCREEN_INTERFACE_NAME, v->ei_touchscreen); + ei_handshake_request_interface_version(setup, EI_TEXT_INTERFACE_NAME, v->ei_text); } ei_handshake_request_finish(setup); @@ -144,6 +145,7 @@ handle_msg_interface_version(struct ei_handshake *setup, const char *name, uint3 else VERSION_UPDATE(ei_button) else VERSION_UPDATE(ei_keyboard) else VERSION_UPDATE(ei_touchscreen) + else VERSION_UPDATE(ei_text) #undef VERSION_UPDATE diff --git a/src/libei-private.h b/src/libei-private.h index 9fec25b..39f6860 100644 --- a/src/libei-private.h +++ b/src/libei-private.h @@ -49,6 +49,7 @@ #include "libei-region.h" #include "libei-scroll.h" #include "libei-seat.h" +#include "libei-text.h" #include "libei-touchscreen.h" struct ei_backend_interface { @@ -77,6 +78,7 @@ struct ei_interface_versions { uint32_t ei_button; uint32_t ei_keyboard; uint32_t ei_touchscreen; + uint32_t ei_text; }; struct ei_unsent { @@ -243,6 +245,13 @@ ei_queue_touch_up_event(struct ei_device *device, uint32_t touchid); void ei_queue_touch_cancel_event(struct ei_device *device, uint32_t touchid); +void +ei_queue_text_keysym_event(struct ei_device *device, uint32_t keysym, + bool is_press); + +void +ei_queue_text_utf8_event(struct ei_device *device, const char *utf8); + void ei_sync_event_send_done(struct ei_event *e); diff --git a/src/libei-seat.c b/src/libei-seat.c index 9d2e49b..dae49dc 100644 --- a/src/libei-seat.c +++ b/src/libei-seat.c @@ -247,6 +247,8 @@ ei_seat_has_capability(struct ei_seat *seat, return seat->capabilities.map[EI_SCROLL_INTERFACE_INDEX] != 0; case EI_DEVICE_CAP_BUTTON: return seat->capabilities.map[EI_BUTTON_INTERFACE_INDEX] != 0; + case EI_DEVICE_CAP_TEXT: + return seat->capabilities.map[EI_TEXT_INTERFACE_INDEX] != 0; } return false; } @@ -281,6 +283,8 @@ ei_seat_cap_mask(struct ei_seat *seat, enum ei_device_capability cap) return seat->capabilities.map[EI_BUTTON_INTERFACE_INDEX]; case EI_DEVICE_CAP_SCROLL: return seat->capabilities.map[EI_SCROLL_INTERFACE_INDEX]; + case EI_DEVICE_CAP_TEXT: + return seat->capabilities.map[EI_TEXT_INTERFACE_INDEX]; } return 0; diff --git a/src/libei-text.c b/src/libei-text.c new file mode 100644 index 0000000..480419d --- /dev/null +++ b/src/libei-text.c @@ -0,0 +1,87 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2025 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#include + +#include "util-bits.h" +#include "util-macros.h" +#include "util-mem.h" +#include "util-io.h" +#include "util-strings.h" +#include "util-version.h" + +#include "libei-private.h" +#include "ei-proto.h" + +static void +ei_text_destroy(struct ei_text *text) +{ + struct ei *ei = ei_text_get_context(text); + ei_unregister_object(ei, &text->proto_object); +} + +OBJECT_IMPLEMENT_REF(ei_text); +OBJECT_IMPLEMENT_UNREF_CLEANUP(ei_text); + +static +OBJECT_IMPLEMENT_CREATE(ei_text); +static +OBJECT_IMPLEMENT_PARENT(ei_text, ei_device); +OBJECT_IMPLEMENT_GETTER_AS_REF(ei_text, proto_object, const struct brei_object*); + +struct ei_device * +ei_text_get_device(struct ei_text *text) +{ + return ei_text_parent(text); +} + +struct ei* +ei_text_get_context(struct ei_text *text) +{ + return ei_device_get_context(ei_text_get_device(text)); +} + +const struct ei_text_interface * +ei_text_get_interface(struct ei_text *text) { + struct ei_device *device = ei_text_get_device(text); + return ei_device_get_text_interface(device); +} + +struct ei_text * +ei_text_new(struct ei_device *device, object_id_t id, uint32_t version) +{ + struct ei_text *text = ei_text_create(&device->object); + struct ei *ei = ei_device_get_context(device); + + text->proto_object.id = id; + text->proto_object.implementation = text; + text->proto_object.interface = &ei_text_proto_interface; + text->proto_object.version = version; + ei_register_object(ei, &text->proto_object); + + return text; /* ref owned by caller */ +} diff --git a/src/libei-text.h b/src/libei-text.h new file mode 100644 index 0000000..8049513 --- /dev/null +++ b/src/libei-text.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2025 Red Hat, Inc. + * + * 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. + */ + +#pragma once + +#include "util-object.h" +#include "util-list.h" +#include "brei-shared.h" + +struct ei; +struct ei_device; +struct ei_text; + +/* This is a protocol-only object, not exposed in the API */ +struct ei_text { + struct object object; + struct brei_object proto_object; +}; + +OBJECT_DECLARE_GETTER(ei_text, context, struct ei*); +OBJECT_DECLARE_GETTER(ei_text, device, struct ei_device*); +OBJECT_DECLARE_GETTER(ei_text, proto_object, const struct brei_object*); +OBJECT_DECLARE_GETTER(ei_text, interface, const struct ei_text_interface *); +OBJECT_DECLARE_REF(ei_text); +OBJECT_DECLARE_UNREF(ei_text); + +struct ei_text * +ei_text_new(struct ei_device *device, object_id_t id, uint32_t version); diff --git a/src/libei.c b/src/libei.c index 6dcbfa7..ad252ee 100644 --- a/src/libei.c +++ b/src/libei.c @@ -133,6 +133,7 @@ ei_create_context(bool is_sender, void *user_data) .ei_scroll = VERSION_V(1), .ei_button = VERSION_V(1), .ei_keyboard = VERSION_V(1), + .ei_text = VERSION_V(1), .ei_touchscreen = VERSION_V(2), }; /* This must be v1 until the server tells us otherwise */ @@ -245,6 +246,8 @@ update_event_timestamp(struct ei_event *event, uint64_t time) case EI_EVENT_TOUCH_DOWN: case EI_EVENT_TOUCH_UP: case EI_EVENT_TOUCH_MOTION: + case EI_EVENT_TEXT_KEYSYM: + case EI_EVENT_TEXT_UTF8: if (event->timestamp != 0) { log_bug(ei_event_get_context(event), "Unexpected timestamp for event of type: %s", @@ -280,6 +283,8 @@ queue_event(struct ei *ei, struct ei_event *event) case EI_EVENT_TOUCH_DOWN: case EI_EVENT_TOUCH_UP: case EI_EVENT_TOUCH_MOTION: + case EI_EVENT_TEXT_KEYSYM: + case EI_EVENT_TEXT_UTF8: prefix = "pending "; queue = &device->pending_event_queue; break; @@ -626,6 +631,30 @@ ei_queue_touch_cancel_event(struct ei_device *device, uint32_t touchid) queue_event(ei_device_get_context(device), e); } +void +ei_queue_text_keysym_event(struct ei_device *device, uint32_t keysym, + bool is_press) +{ + struct ei_event *e = ei_event_new_for_device(device); + + e->type = EI_EVENT_TEXT_KEYSYM; + e->text.keysym = keysym; + e->text.is_press = is_press; + + queue_event(ei_device_get_context(device), e); +} + +void +ei_queue_text_utf8_event(struct ei_device *device, const char *utf8) +{ + struct ei_event *e = ei_event_new_for_device(device); + + e->type = EI_EVENT_TEXT_UTF8; + e->text.utf8 = xstrdup(utf8); + + queue_event(ei_device_get_context(device), e); +} + _public_ void ei_disconnect(struct ei *ei) { diff --git a/src/libei.h b/src/libei.h index 151b24e..28414ac 100644 --- a/src/libei.h +++ b/src/libei.h @@ -300,6 +300,10 @@ enum ei_device_capability { * The device can send button events */ EI_DEVICE_CAP_BUTTON = (1 << 5), + /** + * The device can send text-like data + */ + EI_DEVICE_CAP_TEXT = (1 << 6), }; /** @@ -591,6 +595,24 @@ enum ei_event_type { * See ei_device_touch_new() and ei_touch_motion() for the sender context API. */ EI_EVENT_TOUCH_MOTION, + + /** + * Event for a single keysym logically pressed/released on this device. + * The keysym is an XKB-compatible keysym (not key code!) and may not be + * present on any keymap currently active on any device. + * + * @note This event is only generated on a receiver ei context. + * See ei_device_text_keysym() for the sender context API. + */ + EI_EVENT_TEXT_KEYSYM = 900, + + /** + * Event for a UTF-8 compatible text sequence sent by this device. + * + * @note This event is only generated on a receiver ei context. + * See ei_device_text_utf8() for the sender context API. + */ + EI_EVENT_TEXT_UTF8, }; /** @@ -1845,6 +1867,68 @@ ei_device_scroll_cancel(struct ei_device *device, bool cancel_x, bool cancel_y); void ei_device_keyboard_key(struct ei_device *device, uint32_t keycode, bool is_press); +/** + * @ingroup libei-sender + * + * Generate a key event on a device with + * the @ref EI_DEVICE_CAP_TEXT capability. + * + * Keysyms use the XKB-compatible keysyms, see e.g. + * [/usr/include/xkbcommon/xkbcommon-keysyms.h](https://xkbcommon.org/doc/current/xkbcommon-keysyms_8h.html) + * for a list of key syms + * + * Note that this keysym is independent of any keymap and the keysym + * may not exist on any active keymap on any device. For example, a device + * with both @ref EI_DEVICE_CAP_TEXT and @ref EI_DEVICE_CAP_KEYBOARD is not + * limited to sending keysyms only present on the keymap. + * + * This method is only available on an ei sender context. + * + * @param device The EI device + * @param keysym The keysym + * @param is_press true for key down, false for key up + */ +void +ei_device_text_keysym(struct ei_device *device, uint32_t keysym, bool is_press); + +/** + * @ingroup libei-sender + * + * Generate a UTF-8 text event on a device with + * the @ref EI_DEVICE_CAP_TEXT capability. + * + * This method is only available on an ei sender context. + * This method is identical to ei_device_text_utf8_with_length() + * but calculates the length of the string. + * + * @param device The EI device + * @param text The zero-terminated UTF-8 compatible text + * + * @see ei_device_text_utf8_with_length + */ +void +ei_device_text_utf8(struct ei_device *device, const char *text); + +/** + * @ingroup libei-sender + * + * Generate a UTF-8 text event on a device with + * the @ref EI_DEVICE_CAP_TEXT capability. + * + * This method is only available on an ei sender context. + * + * This method is identical to ei_device_text_utf8() + * but takes an additional length argument. + * + * @param device The EI device + * @param text The zero-terminated UTF-8 compatible text + * @param length The length of text in bytes, excluding the zero bytes + * + * @see ei_device_text_utf8 + */ +void +ei_device_text_utf8_with_length(struct ei_device *device, const char *text, size_t length); + /** * @ingroup libei-sender * @@ -2220,6 +2304,34 @@ ei_event_touch_get_y(struct ei_event *event); bool ei_event_touch_get_is_cancel(struct ei_event *event); +/** + * @ingroup libei-receiver + * + * For an event of type @ref EI_EVENT_TEXT_KEYSYM + * return the XKB-compatible keysym. + */ +uint32_t +ei_event_text_get_keysym(struct ei_event *event); + +/** + * @ingroup libei-receiver + * + * For an event of type @ref EI_EVENT_TEXT_KEYSYM + * return true if the event is a logical key down for the + * keysym. + */ +bool +ei_event_text_get_keysym_is_press(struct ei_event *event); + +/** + * @ingroup libei-receiver + * + * For an event of type @ref EI_EVENT_TEXT_UTF8 + * return the zero-terminated UTF8 string. + */ +const char * +ei_event_text_get_utf8(struct ei_event *event); + /** * @} */ diff --git a/src/libeis-client.c b/src/libeis-client.c index f0f7a67..f9b5215 100644 --- a/src/libeis-client.c +++ b/src/libeis-client.c @@ -528,6 +528,7 @@ eis_client_new(struct eis *eis, int fd) .ei_scroll = VERSION_V(1), .ei_button = VERSION_V(1), .ei_keyboard = VERSION_V(1), + .ei_text = VERSION_V(1), .ei_touchscreen = VERSION_V(2), }; struct source *s = source_new(fd, client_dispatch, client); diff --git a/src/libeis-client.h b/src/libeis-client.h index 14b4833..9a33cd1 100644 --- a/src/libeis-client.h +++ b/src/libeis-client.h @@ -52,6 +52,7 @@ struct eis_client_interface_versions { uint32_t ei_button; uint32_t ei_keyboard; uint32_t ei_touchscreen; + uint32_t ei_text; }; struct eis_client { diff --git a/src/libeis-device.c b/src/libeis-device.c index 514ef12..3fd0f32 100644 --- a/src/libeis-device.c +++ b/src/libeis-device.c @@ -149,6 +149,7 @@ eis_device_destroy(struct eis_device *device) eis_pointer_unref(device->pointer); eis_touchscreen_unref(device->touchscreen); eis_keyboard_unref(device->keyboard); + eis_text_unref(device->text); free(device->name); } @@ -739,6 +740,73 @@ eis_device_get_touchscreen_interface(struct eis_device *device) return &touchscreen_interface; } +static struct brei_result * +client_msg_text_release(struct eis_text *text) +{ + struct eis_device *device = eis_text_get_device(text); + eis_text_event_destroyed(device->text, + eis_client_get_next_serial(eis_device_get_client(device))); + eis_text_unref(steal(&device->text)); + return NULL; +} + +static struct brei_result * +client_msg_text_keysym(struct eis_text *text, uint32_t keysym, uint32_t state) +{ + struct eis_device *device = eis_text_get_device(text); + + DISCONNECT_IF_RECEIVER_CONTEXT(device); + + if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) { + return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "keysym event for non-text device"); + } + + if (device->state == EIS_DEVICE_STATE_EMULATING) { + eis_queue_text_keysym_event(device, keysym, !!state); + return NULL; + } + + return maybe_error_on_device_state(device, "text keysym"); +} + +static struct brei_result * +client_msg_text_utf8(struct eis_text *text, const char *utf8) +{ + struct eis_device *device = eis_text_get_device(text); + + DISCONNECT_IF_RECEIVER_CONTEXT(device); + + if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) { + return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "utf8 event for non-text device"); + } + + if (!utf8) { + return brei_result_new(EIS_CONNECTION_DISCONNECT_REASON_PROTOCOL, + "utf8 text is NULL"); + } + + if (device->state == EIS_DEVICE_STATE_EMULATING) { + eis_queue_text_utf8_event(device, utf8); + return NULL; + } + + return maybe_error_on_device_state(device, "text utf8"); +} + +static const struct eis_text_interface text_interface = { + .release = client_msg_text_release, + .keysym = client_msg_text_keysym, + .utf8 = client_msg_text_utf8, +}; + +const struct eis_text_interface * +eis_device_get_text_interface(struct eis_device *device) +{ + return &text_interface; +} + _public_ struct eis_device * eis_seat_new_device(struct eis_seat *seat) { @@ -809,6 +877,7 @@ eis_device_configure_capability(struct eis_device *device, enum eis_device_capab case EIS_DEVICE_CAP_TOUCH: case EIS_DEVICE_CAP_BUTTON: case EIS_DEVICE_CAP_SCROLL: + case EIS_DEVICE_CAP_TEXT: mask_add(device->capabilities, cap); break; } @@ -935,6 +1004,14 @@ eis_device_add(struct eis_device *device) if (rc < 0) goto out; } + if (eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) { + device->text = eis_text_new(device); + rc = eis_device_event_interface(device, eis_text_get_id(device->text), + EIS_TEXT_INTERFACE_NAME, + eis_text_get_version(device->text)); + if (rc < 0) + goto out; + } rc = eis_device_event_done(device); if (rc < 0) @@ -991,6 +1068,10 @@ eis_device_remove(struct eis_device *device) eis_keyboard_event_destroyed(device->keyboard, eis_client_get_next_serial(client)); eis_keyboard_unref(steal(&device->keyboard)); } + if (device->text) { + eis_text_event_destroyed(device->text, eis_client_get_next_serial(client)); + eis_text_unref(steal(&device->text)); + } if (device->state != EIS_DEVICE_STATE_NEW) eis_device_event_destroyed(device, eis_client_get_next_serial(client)); @@ -1018,6 +1099,7 @@ eis_device_has_capability(struct eis_device *device, case EIS_DEVICE_CAP_TOUCH: case EIS_DEVICE_CAP_BUTTON: case EIS_DEVICE_CAP_SCROLL: + case EIS_DEVICE_CAP_TEXT: return mask_all(device->capabilities, cap); } return false; @@ -1391,6 +1473,49 @@ eis_touch_cancel(struct eis_touch *touch) eis_touchscreen_event_up(device->touchscreen, touch->tracking_id); } +_public_ void +eis_device_text_keysym(struct eis_device *device, + uint32_t keysym, bool is_press) +{ + if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) { + log_bug_client(eis_device_get_context(device), + "%s: device is not a text device", __func__); + return; + } + + if (device->state != EIS_DEVICE_STATE_EMULATING) + return; + + device->send_frame_event = true; + + eis_text_event_keysym(device->text, keysym, is_press); +} + +_public_ void +eis_device_text_utf8(struct eis_device *device, const char *utf8) +{ + return eis_device_text_utf8_with_length(device, utf8, utf8 ? strlen(utf8) : 0); +} + +_public_ void +eis_device_text_utf8_with_length(struct eis_device *device, const char *text, size_t length) +{ + if (!eis_device_has_capability(device, EIS_DEVICE_CAP_TEXT)) { + log_bug_client(eis_device_get_context(device), + "%s: device is not a text device", __func__); + return; + } + + if (device->state != EIS_DEVICE_STATE_EMULATING) + return; + + device->send_frame_event = true; + + char buf[4096] = {0}; + memcpy(buf, text, min(length, sizeof(buf) - 1)); + eis_text_event_utf8(device->text, buf); +} + _public_ void eis_device_frame(struct eis_device *device, uint64_t time) { diff --git a/src/libeis-device.h b/src/libeis-device.h index 5f6f2a1..f500d72 100644 --- a/src/libeis-device.h +++ b/src/libeis-device.h @@ -52,6 +52,7 @@ struct eis_device { struct eis_button *button; struct eis_keyboard *keyboard; struct eis_touchscreen *touchscreen; + struct eis_text *text; char *name; enum eis_device_state state; @@ -117,6 +118,7 @@ OBJECT_DECLARE_GETTER(eis_device, scroll_interface, const struct eis_scroll_inte OBJECT_DECLARE_GETTER(eis_device, button_interface, const struct eis_button_interface *); OBJECT_DECLARE_GETTER(eis_device, keyboard_interface, const struct eis_keyboard_interface *); OBJECT_DECLARE_GETTER(eis_device, touchscreen_interface, const struct eis_touchscreen_interface *); +OBJECT_DECLARE_GETTER(eis_device, text_interface, const struct eis_text_interface *); void eis_device_set_client_keymap(struct eis_device *device, diff --git a/src/libeis-event.c b/src/libeis-event.c index 125dc51..ee0fe62 100644 --- a/src/libeis-event.c +++ b/src/libeis-event.c @@ -57,6 +57,11 @@ eis_event_destroy(struct eis_event *event) case EIS_EVENT_TOUCH_MOTION: case EIS_EVENT_TOUCH_UP: case EIS_EVENT_FRAME: + case EIS_EVENT_TEXT_KEYSYM: + handled = true; + break; + case EIS_EVENT_TEXT_UTF8: + free(steal(&event->text.utf8)); handled = true; break; case EIS_EVENT_PONG: @@ -192,6 +197,8 @@ eis_event_get_time(struct eis_event *event) EIS_EVENT_TOUCH_DOWN, EIS_EVENT_TOUCH_UP, EIS_EVENT_TOUCH_MOTION, + EIS_EVENT_TEXT_KEYSYM, + EIS_EVENT_TEXT_UTF8, EIS_EVENT_FRAME); return event->timestamp; @@ -217,6 +224,7 @@ eis_event_seat_has_capability(struct eis_event *event, enum eis_device_capabilit case EIS_DEVICE_CAP_TOUCH: case EIS_DEVICE_CAP_BUTTON: case EIS_DEVICE_CAP_SCROLL: + case EIS_DEVICE_CAP_TEXT: return mask_all(event->bind.capabilities, cap); } return false; @@ -430,3 +438,30 @@ eis_event_touch_get_is_cancel(struct eis_event *event) return event->touch.is_cancel; } + +_public_ uint32_t +eis_event_text_get_keysym(struct eis_event *event) +{ + require_event_type(event, 0, + EIS_EVENT_TEXT_KEYSYM); + + return event->text.keysym; +} + +_public_ bool +eis_event_text_get_keysym_is_press(struct eis_event *event) +{ + require_event_type(event, false, + EIS_EVENT_TEXT_KEYSYM); + + return event->text.is_press; +} + +_public_ const char * +eis_event_text_get_utf8(struct eis_event *event) +{ + require_event_type(event, NULL, + EIS_EVENT_TEXT_UTF8); + + return event->text.utf8; +} diff --git a/src/libeis-event.h b/src/libeis-event.h index 9754d22..250554e 100644 --- a/src/libeis-event.h +++ b/src/libeis-event.h @@ -60,6 +60,11 @@ struct eis_event { double x, y; bool is_cancel; } touch; + struct { + uint32_t keysym; + bool is_press; + char *utf8; + } text; struct { uint32_t sequence; } start_emulating; diff --git a/src/libeis-handshake.c b/src/libeis-handshake.c index 39367b6..8e772cd 100644 --- a/src/libeis-handshake.c +++ b/src/libeis-handshake.c @@ -237,6 +237,7 @@ client_msg_interface_version(struct eis_handshake *setup, const char *name, uint VERSION_ENTRY(ei_scroll), VERSION_ENTRY(ei_keyboard), VERSION_ENTRY(ei_touchscreen), + VERSION_ENTRY(ei_text), #undef VERSION_ENTRY }; diff --git a/src/libeis-private.h b/src/libeis-private.h index b3634ef..85d61ff 100644 --- a/src/libeis-private.h +++ b/src/libeis-private.h @@ -51,6 +51,7 @@ #include "libeis-region.h" #include "libeis-scroll.h" #include "libeis-seat.h" +#include "libeis-text.h" #include "libeis-touchscreen.h" struct eis_backend_interface { @@ -160,6 +161,12 @@ eis_queue_touch_up_event(struct eis_device *device, uint32_t touchid); void eis_queue_touch_cancel_event(struct eis_device *device, uint32_t touchid); +void +eis_queue_text_keysym_event(struct eis_device *device, uint32_t keysym, bool is_press); + +void +eis_queue_text_utf8_event(struct eis_device *device, const char *utf8); + void eis_sync_event_send_done(struct eis_event *e); diff --git a/src/libeis-seat.c b/src/libeis-seat.c index bca387e..4b673c8 100644 --- a/src/libeis-seat.c +++ b/src/libeis-seat.c @@ -108,6 +108,8 @@ client_msg_bind(struct eis_seat *seat, uint64_t caps) capabilities |= EIS_DEVICE_CAP_BUTTON; if (caps & bit(EIS_SCROLL_INTERFACE_INDEX)) capabilities |= EIS_DEVICE_CAP_SCROLL; + if (caps & bit(EIS_TEXT_INTERFACE_INDEX)) + capabilities |= EIS_DEVICE_CAP_TEXT; eis_seat_bind(seat, capabilities); @@ -221,6 +223,14 @@ eis_seat_add(struct eis_seat *seat) mask_add(seat->capabilities.proto_mask, mask); } + if (seat->capabilities.c_mask & EIS_DEVICE_CAP_TEXT && + client->interface_versions.ei_text > 0) { + uint64_t mask = bit(EIS_TEXT_INTERFACE_INDEX); + eis_seat_event_capability(seat, mask, + EIS_TEXT_INTERFACE_NAME); + mask_add(seat->capabilities.proto_mask, mask); + } + eis_seat_event_done(seat); } @@ -313,6 +323,7 @@ eis_seat_configure_capability(struct eis_seat *seat, case EIS_DEVICE_CAP_TOUCH: case EIS_DEVICE_CAP_BUTTON: case EIS_DEVICE_CAP_SCROLL: + case EIS_DEVICE_CAP_TEXT: mask_add(seat->capabilities.c_mask, cap); break; } @@ -329,6 +340,7 @@ eis_seat_has_capability(struct eis_seat *seat, case EIS_DEVICE_CAP_TOUCH: case EIS_DEVICE_CAP_BUTTON: case EIS_DEVICE_CAP_SCROLL: + case EIS_DEVICE_CAP_TEXT: return mask_all(seat->capabilities.c_mask, cap); } return false; diff --git a/src/libeis-text.c b/src/libeis-text.c new file mode 100644 index 0000000..0654d1e --- /dev/null +++ b/src/libeis-text.c @@ -0,0 +1,107 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2025 Red Hat, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#include +#include + +#include "util-bits.h" +#include "util-macros.h" +#include "util-mem.h" +#include "util-io.h" +#include "util-strings.h" +#include "util-version.h" + +#include "libeis-private.h" +#include "eis-proto.h" + +static void +eis_text_destroy(struct eis_text *text) +{ + struct eis_client * client = eis_text_get_client(text); + eis_client_unregister_object(client, &text->proto_object); +} + +OBJECT_IMPLEMENT_REF(eis_text); +OBJECT_IMPLEMENT_UNREF_CLEANUP(eis_text); +OBJECT_IMPLEMENT_GETTER_AS_REF(eis_text, proto_object, const struct brei_object *); + +static +OBJECT_IMPLEMENT_CREATE(eis_text); +static +OBJECT_IMPLEMENT_PARENT(eis_text, eis_device); + +uint32_t +eis_text_get_version(struct eis_text *text) +{ + return text->proto_object.version; +} + +object_id_t +eis_text_get_id(struct eis_text *text) +{ + return text->proto_object.id; +} + +struct eis_device * +eis_text_get_device(struct eis_text *text) +{ + return eis_text_parent(text); +} + +struct eis_client* +eis_text_get_client(struct eis_text *text) +{ + return eis_device_get_client(eis_text_get_device(text)); +} + +struct eis* +eis_text_get_context(struct eis_text *text) +{ + struct eis_client *client = eis_text_get_client(text); + return eis_client_get_context(client); +} + +const struct eis_text_interface * +eis_text_get_interface(struct eis_text *text) { + return eis_device_get_text_interface(eis_text_get_device(text)); +} + +struct eis_text * +eis_text_new(struct eis_device *device) +{ + struct eis_text *text = eis_text_create(&device->object); + struct eis_client *client = eis_device_get_client(device); + + text->proto_object.id = eis_client_get_new_id(client); + text->proto_object.implementation = text; + text->proto_object.interface = &eis_text_proto_interface; + text->proto_object.version = client->interface_versions.ei_text; + list_init(&text->proto_object.link); + + eis_client_register_object(client, &text->proto_object); + + return text; /* ref owned by caller */ +} diff --git a/src/libeis-text.h b/src/libeis-text.h new file mode 100644 index 0000000..14eef7a --- /dev/null +++ b/src/libeis-text.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: MIT */ +/* + * Copyright © 2025 Red Hat, Inc. + * + * 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. + */ + +#pragma once + +#include "util-object.h" +#include "brei-shared.h" +#include "libeis-client.h" + +struct eis; +struct eis_client; + +/* This is a protocol-only object, not exposed in the API */ +struct eis_text { + struct object object; + struct brei_object proto_object; +}; + +OBJECT_DECLARE_GETTER(eis_text, context, struct eis *); +OBJECT_DECLARE_GETTER(eis_text, device, struct eis_device *); +OBJECT_DECLARE_GETTER(eis_text, client, struct eis_client *); +OBJECT_DECLARE_GETTER(eis_text, id, object_id_t); +OBJECT_DECLARE_GETTER(eis_text, version, uint32_t); +OBJECT_DECLARE_GETTER(eis_text, proto_object, const struct brei_object *); +OBJECT_DECLARE_GETTER(eis_text, interface, const struct eis_text_interface *); +OBJECT_DECLARE_REF(eis_text); +OBJECT_DECLARE_UNREF(eis_text); + +struct eis_text * +eis_text_new(struct eis_device *device); diff --git a/src/libeis.c b/src/libeis.c index a83d465..adec8e0 100644 --- a/src/libeis.c +++ b/src/libeis.c @@ -155,6 +155,8 @@ eis_event_type_to_string(enum eis_event_type type) CASE_RETURN_STRING(EIS_EVENT_TOUCH_UP); CASE_RETURN_STRING(EIS_EVENT_TOUCH_MOTION); CASE_RETURN_STRING(EIS_EVENT_FRAME); + CASE_RETURN_STRING(EIS_EVENT_TEXT_KEYSYM); + CASE_RETURN_STRING(EIS_EVENT_TEXT_UTF8); } return NULL; @@ -175,6 +177,8 @@ update_event_timestamp(struct eis_event *event, uint64_t time) case EIS_EVENT_TOUCH_DOWN: case EIS_EVENT_TOUCH_UP: case EIS_EVENT_TOUCH_MOTION: + case EIS_EVENT_TEXT_KEYSYM: + case EIS_EVENT_TEXT_UTF8: if (event->timestamp != 0) { log_bug(eis_event_get_context(event), "Unexpected timestamp for event of type: %s", @@ -211,6 +215,8 @@ eis_queue_event(struct eis_event *event) case EIS_EVENT_TOUCH_DOWN: case EIS_EVENT_TOUCH_UP: case EIS_EVENT_TOUCH_MOTION: + case EIS_EVENT_TEXT_KEYSYM: + case EIS_EVENT_TEXT_UTF8: prefix = "pending "; queue = &device->pending_event_queue; break; @@ -474,6 +480,26 @@ eis_queue_touch_cancel_event(struct eis_device *device, uint32_t touchid) eis_queue_event(e); } +void +eis_queue_text_keysym_event(struct eis_device *device, uint32_t keysym, + bool is_press) +{ + struct eis_event *e = eis_event_new_for_device(device); + e->type = EIS_EVENT_TEXT_KEYSYM; + e->text.keysym = keysym; + e->text.is_press = is_press; + eis_queue_event(e); +} + +void +eis_queue_text_utf8_event(struct eis_device *device, const char *text) +{ + struct eis_event *e = eis_event_new_for_device(device); + e->type = EIS_EVENT_TEXT_UTF8; + e->text.utf8 = xstrdup(text); + eis_queue_event(e); +} + _public_ struct eis_event* eis_get_event(struct eis *eis) { diff --git a/src/libeis.h b/src/libeis.h index 17b2619..da5d13b 100644 --- a/src/libeis.h +++ b/src/libeis.h @@ -226,6 +226,7 @@ enum eis_device_capability { EIS_DEVICE_CAP_TOUCH = (1 << 3), EIS_DEVICE_CAP_SCROLL = (1 << 4), EIS_DEVICE_CAP_BUTTON = (1 << 5), + EIS_DEVICE_CAP_TEXT = (1 << 6), }; /** @@ -396,6 +397,16 @@ enum eis_event_type { * properties). */ EIS_EVENT_TOUCH_MOTION, + + /** + * A key sym logical press or release event + */ + EIS_EVENT_TEXT_KEYSYM = 900, + + /** + * A text event event + */ + EIS_EVENT_TEXT_UTF8, }; /** @@ -1508,6 +1519,30 @@ eis_device_keyboard_send_xkb_modifiers(struct eis_device *device, uint32_t locked, uint32_t group); +/** + * @ingroup libeis-receiver + * + * see @ref ei_device_text_keysym + */ +void +eis_device_text_keysym(struct eis_device *device, uint32_t keysym, bool is_press); + +/** + * @ingroup libeis-receiver + * + * see @ref ei_device_text_utf8 + */ +void +eis_device_text_utf8(struct eis_device *device, const char *text); + +/** + * @ingroup libeis-receiver + * + * see @ref ei_device_text_utf8_with_length + */ +void +eis_device_text_utf8_with_length(struct eis_device *device, const char *text, size_t length); + /** * @ingroup libeis-receiver * @@ -1869,6 +1904,34 @@ eis_event_touch_get_y(struct eis_event *event); bool eis_event_touch_get_is_cancel(struct eis_event *event); +/** + * @ingroup libeis-sender + * + * For an event of type @ref EIS_EVENT_TEXT_KEYSYM + * return the XKB-compatible keysym. + */ +uint32_t +eis_event_text_get_keysym(struct eis_event *event); + +/** + * @ingroup libeis-sender + * + * For an event of type @ref EIS_EVENT_TEXT_KEYSYM + * return true if the event is a logical key down for the + * keysym. + */ +bool +eis_event_text_get_keysym_is_press(struct eis_event *event); + +/** + * @ingroup libeis-sender + * + * For an event of type @ref EIS_EVENT_TEXT_UTF8 + * return the zero-terminated UTF8 string. + */ +const char * +eis_event_text_get_utf8(struct eis_event *event); + /** * @returns a timestamp for the current time to pass into * eis_device_frame(). diff --git a/src/meson.build b/src/meson.build index db5785e..2da91ad 100644 --- a/src/meson.build +++ b/src/meson.build @@ -79,6 +79,7 @@ if build_libei 'libei-scroll.c', 'libei-seat.c', 'libei-socket.c', + 'libei-text.c', 'libei-touchscreen.c', ) + [brei_proto_headers, ei_proto_headers, ei_proto_sources] @@ -157,6 +158,7 @@ if build_libeis 'libeis-scroll.c', 'libeis-seat.c', 'libeis-socket.c', + 'libeis-text.c', 'libeis-touchscreen.c', 'libeis.c', ) + [brei_proto_headers, eis_proto_headers, eis_proto_sources] diff --git a/test/eierpecken.c b/test/eierpecken.c index e8fdd8d..1561d5e 100644 --- a/test/eierpecken.c +++ b/test/eierpecken.c @@ -1159,6 +1159,8 @@ _peck_dispatch_eis(struct peck *peck, int lineno) case EIS_EVENT_TOUCH_DOWN: case EIS_EVENT_TOUCH_UP: case EIS_EVENT_TOUCH_MOTION: + case EIS_EVENT_TEXT_KEYSYM: + case EIS_EVENT_TEXT_UTF8: need_frame = true; break; } @@ -1320,6 +1322,8 @@ _peck_dispatch_ei(struct peck *peck, int lineno) case EI_EVENT_TOUCH_DOWN: case EI_EVENT_TOUCH_UP: case EI_EVENT_TOUCH_MOTION: + case EI_EVENT_TEXT_KEYSYM: + case EI_EVENT_TEXT_UTF8: need_frame = true; break; } @@ -1656,6 +1660,8 @@ peck_ei_event_type_name(enum ei_event_type type) CASE_STRING(TOUCH_DOWN); CASE_STRING(TOUCH_UP); CASE_STRING(TOUCH_MOTION); + CASE_STRING(TEXT_KEYSYM); + CASE_STRING(TEXT_UTF8); } #undef CASE_STRING assert(!"Unhandled ei event type"); @@ -1695,6 +1701,8 @@ peck_eis_event_type_name(enum eis_event_type type) CASE_STRING(TOUCH_UP); CASE_STRING(TOUCH_MOTION); CASE_STRING(FRAME); + CASE_STRING(TEXT_KEYSYM); + CASE_STRING(TEXT_UTF8); } #undef CASE_STRING assert(!"Unhandled EIS event type"); diff --git a/tools/ei-debug-events.c b/tools/ei-debug-events.c index 02bfeb8..2a364d8 100644 --- a/tools/ei-debug-events.c +++ b/tools/ei-debug-events.c @@ -51,6 +51,10 @@ #define libevdev_event_code_get_name(...) "" #endif +#if HAVE_LIBXKBCOMMON +#include +#endif + #include "libei.h" #include "src/util-macros.h" @@ -227,6 +231,32 @@ print_key_event(struct ei_event *event) printf(" press: %s\n", press ? "true" : "false"); } +static void +print_keysym_event(struct ei_event *event) +{ + print_device(event); + + uint32_t keysym = ei_event_text_get_keysym(event); + bool press = ei_event_text_get_keysym_is_press(event); + + char buf[128] = {0}; +#if HAVE_LIBXKBCOMMON + xkb_keysym_get_name(keysym, buf, sizeof(buf)); +#else + snprintf(buf, sizeof(buf), "0x%04x", keysym); +#endif + + printf(" keysym: 0x%08x # %s\n", keysym, buf); + printf(" press: %s\n", press ? "true" : "false"); +} + +static void +print_utf8_event(struct ei_event *event) +{ + const char *text = ei_event_text_get_utf8(event); + printf(" utf8: '%s'\n", text); +} + static void print_touch_event(struct ei_event *event) { @@ -434,6 +464,12 @@ int main(int argc, char **argv) case EI_EVENT_KEYBOARD_KEY: print_key_event(e); break; + case EI_EVENT_TEXT_KEYSYM: + print_keysym_event(e); + break; + case EI_EVENT_TEXT_UTF8: + print_utf8_event(e); + break; case EI_EVENT_TOUCH_DOWN: case EI_EVENT_TOUCH_MOTION: case EI_EVENT_TOUCH_UP: diff --git a/tools/ei-demo-client.c b/tools/ei-demo-client.c index 45114cc..95807a9 100644 --- a/tools/ei-demo-client.c +++ b/tools/ei-demo-client.c @@ -51,6 +51,10 @@ #include #endif +#ifndef XK_dead_a +#define XK_dead_a 0xfe80 +#endif + #include "libei.h" #include "src/util-macros.h" @@ -126,7 +130,7 @@ setup_xkb_keymap(struct ei_keymap *keymap) for (unsigned int evcode = KEY_Q; evcode <= KEY_Y; evcode++) { char utf8[7]; xkb_keysym_t keysym = xkb_state_key_get_one_sym(xkbstate, evcode + 8); - xkb_keysym_to_utf8(keysym, utf8, sizeof(utf8)); + xkb_keysym_get_name(keysym, utf8, sizeof(utf8)); strcat(layout, utf8); } @@ -300,12 +304,14 @@ int main(int argc, char **argv) _unref_(ei_device) *kbd = NULL; _unref_(ei_device) *abs = NULL; _unref_(ei_device) *touch = NULL; + _unref_(ei_device) *text = NULL; bool stop = false; bool have_ptr = false; bool have_kbd = false; bool have_abs = false; bool have_touch = false; + bool have_text = false; struct ei_seat *default_seat = NULL; uint32_t sequence = 0; @@ -344,7 +350,9 @@ int main(int argc, char **argv) EI_DEVICE_CAP_POINTER_ABSOLUTE, EI_DEVICE_CAP_TOUCH, EI_DEVICE_CAP_BUTTON, - EI_DEVICE_CAP_SCROLL, NULL); + EI_DEVICE_CAP_SCROLL, + EI_DEVICE_CAP_TEXT, + NULL); break; } case EI_EVENT_SEAT_REMOVED: @@ -376,6 +384,10 @@ int main(int argc, char **argv) touch = ei_device_ref(device); handle_regions(device); } + if (ei_device_has_capability(device, EI_DEVICE_CAP_TEXT)) { + colorprint("New text device: %s\n", ei_device_get_name(device)); + text = ei_device_ref(device); + } } break; case EI_EVENT_DEVICE_RESUMED: @@ -403,6 +415,12 @@ int main(int argc, char **argv) colorprint("Touch device was resumed\n"); have_touch = true; } + if (ei_event_get_device(e) == text) { + if (!receiver) + ei_device_start_emulating(text, ++sequence); + colorprint("Text device was resumed\n"); + have_text = true; + } break; case EI_EVENT_DEVICE_PAUSED: if (ei_event_get_device(e) == ptr) { @@ -421,6 +439,10 @@ int main(int argc, char **argv) colorprint("Touch device was paused\n"); have_touch = false; } + if (ei_event_get_device(e) == text) { + colorprint("Text device was paused\n"); + have_text = false; + } break; case EI_EVENT_DEVICE_REMOVED: { @@ -498,6 +520,26 @@ int main(int argc, char **argv) colorprint("touch up %u\n", ei_event_touch_get_id(e)); } break; + case EI_EVENT_TEXT_KEYSYM: + { + char buf[128]; + uint32_t keysym = ei_event_text_get_keysym(e); +#if HAVE_LIBXKBCOMMON + xkb_keysym_to_utf8(keysym, buf, sizeof(buf)); +#else + snprintf(buf, sizeof(buf), "0x%04x", keysym); +#endif + colorprint("text keysym %u [%s] (%s)\n", + keysym, + buf, + ei_event_text_get_keysym_is_press(e) ? "press" : "release"); + } + break; + case EI_EVENT_TEXT_UTF8: + { + colorprint("text utf8 '%s'\n", ei_event_text_get_utf8(e)); + } + break; case EI_EVENT_SYNC: { colorprint("sync\n"); @@ -585,6 +627,23 @@ int main(int argc, char **argv) } } + + if (have_text) { + static int key = 0; + colorprint("sending text event\n"); + ei_device_text_keysym(text, XK_dead_a + key, false); + ei_device_frame(text, now); + now += interval; + ei_device_text_keysym(text, XK_dead_a + key, false); + ei_device_frame(text, now); + now += interval; + key = (key + 1) % 6; + + colorprint("sending text utf8 event\n"); + ei_device_text_utf8(text, "👋 World!"); + ei_device_frame(text, now); + now += interval; + } } } @@ -597,13 +656,17 @@ int main(int argc, char **argv) ei_device_close(abs); if (touch) ei_device_close(touch); + if (text) + ei_device_close(text); if (default_seat) { ei_seat_bind_capabilities(default_seat, EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_KEYBOARD, EI_DEVICE_CAP_POINTER_ABSOLUTE, EI_DEVICE_CAP_TOUCH, EI_DEVICE_CAP_BUTTON, - EI_DEVICE_CAP_SCROLL, NULL); + EI_DEVICE_CAP_SCROLL, + EI_DEVICE_CAP_TEXT, + NULL); ei_seat_unref(default_seat); } diff --git a/tools/eis-demo-server.c b/tools/eis-demo-server.c index 5f19c5f..fdc56b4 100644 --- a/tools/eis-demo-server.c +++ b/tools/eis-demo-server.c @@ -54,6 +54,10 @@ #include #endif +#ifndef XK_ssharp +#define XK_ssharp 0x00df /* U+00DF LATIN SMALL LETTER SHARP S */ +#endif + #include "src/util-color.h" #include "src/util-mem.h" #include "src/util-memfile.h" @@ -124,6 +128,7 @@ eis_demo_client_destroy(struct eis_demo_client *democlient) eis_device_unref(democlient->abs); eis_device_unref(democlient->kbd); eis_device_unref(democlient->touchscreen); + eis_device_unref(democlient->text); } static @@ -319,6 +324,17 @@ add_device(struct eis_demo_server *server, struct eis_client *client, case EIS_DEVICE_CAP_SCROLL: /* Mixed in with pointer/abs - good enough for a demo server */ break; + case EIS_DEVICE_CAP_TEXT: + { + struct eis_device *text = eis_seat_new_device(seat); + eis_device_configure_name(text, "test text device"); + eis_device_configure_capability(text, EIS_DEVICE_CAP_TEXT); + colorprint("Creating text device %s for %s\n", eis_device_get_name(text), + eis_client_get_name(client)); + eis_device_add(text); + device = steal(&text); + break; + } } return device; @@ -371,6 +387,7 @@ eis_demo_server_printf_handle_event(struct eis_demo_server *server, eis_seat_configure_capability(seat, EIS_DEVICE_CAP_TOUCH); eis_seat_configure_capability(seat, EIS_DEVICE_CAP_BUTTON); eis_seat_configure_capability(seat, EIS_DEVICE_CAP_SCROLL); + eis_seat_configure_capability(seat, EIS_DEVICE_CAP_TEXT); eis_seat_add(seat); /* Note: we don't have a ref to this seat ourselves anywhere */ break; @@ -436,12 +453,23 @@ eis_demo_server_printf_handle_event(struct eis_demo_server *server, } } + if (eis_event_seat_has_capability(e, EIS_DEVICE_CAP_TEXT)) { + if (!democlient->text) + democlient->text = add_device(server, client, seat, EIS_DEVICE_CAP_TEXT); + } else { + if (democlient->text) { + eis_device_remove(democlient->text); + democlient->text = eis_device_unref(democlient->text); + } + } + /* Special "Feature", if all caps are unbound remove the seat. * This is a demo server after all, so let's demo this. */ if (!eis_event_seat_has_capability(e, EIS_DEVICE_CAP_POINTER) && !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_POINTER_ABSOLUTE) && !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_KEYBOARD) && - !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_TOUCH)) + !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_TOUCH) && + !eis_event_seat_has_capability(e, EIS_DEVICE_CAP_TEXT)) eis_seat_remove(seat); break; @@ -479,6 +507,9 @@ eis_demo_server_printf_handle_event(struct eis_demo_server *server, if (democlient->touchscreen == device) democlient->touchscreen = NULL; + if (democlient->text == device) + democlient->text = NULL; + eis_device_unref(device); } break; @@ -551,6 +582,27 @@ eis_demo_server_printf_handle_event(struct eis_demo_server *server, colorprint("touch up %u\n", eis_event_touch_get_id(e)); } break; + case EIS_EVENT_TEXT_KEYSYM: + { + char buf[128] = {0}; + uint32_t keysym = eis_event_text_get_keysym(e); +#if HAVE_LIBXKBCOMMON + xkb_keysym_get_name(keysym, buf, sizeof(buf)); +#else + snprintf(buf, sizeof(buf), "0x%04x", keysym); +#endif + colorprint("text keysym %u [%s] (%s)\n", + keysym, + buf, + eis_event_text_get_keysym_is_press(e) ? "press" : "release"); + } + break; + case EIS_EVENT_TEXT_UTF8: + { + const char *text = eis_event_text_get_utf8(e); + colorprint("text utf8 '%s'\n", text); + } + break; case EIS_EVENT_FRAME: { colorprint("frame timestamp: %" PRIu64 "\n", @@ -744,6 +796,7 @@ int main(int argc, char **argv) struct eis_device *kbd = democlient->kbd; struct eis_device *abs = democlient->abs; struct eis_device *touchscreen = democlient->touchscreen; + struct eis_device *text = democlient->text; if (ptr) { colorprint("sending motion event\n"); eis_device_pointer_motion(ptr, -1, 1); @@ -816,6 +869,23 @@ int main(int argc, char **argv) } } + + if (text) { + static int key = 0; + colorprint("sending text event\n"); + + eis_device_text_keysym(text, XK_ssharp + key, true); /* KEY_Q */ + eis_device_frame(text, now); + now += interval; + eis_device_text_keysym(text, XK_ssharp + key, false); /* KEY_Q */ + eis_device_frame(text, now); + now += interval; + key = (key + 1) % 6; + + eis_device_text_utf8(text, "👋🏽 World"); + eis_device_frame(text, now); + now += interval; + } } } diff --git a/tools/eis-demo-server.h b/tools/eis-demo-server.h index 18a1bac..a17bd81 100644 --- a/tools/eis-demo-server.h +++ b/tools/eis-demo-server.h @@ -40,6 +40,7 @@ struct eis_demo_client { struct eis_device *abs; struct eis_device *touchscreen; struct eis_touch *touch; + struct eis_device *text; }; struct eis_demo_server { diff --git a/tools/meson.build b/tools/meson.build index 0573ccf..ce75bda 100644 --- a/tools/meson.build +++ b/tools/meson.build @@ -29,7 +29,12 @@ if build_libei executable('ei-debug-events', 'ei-debug-events.c', - dependencies: [dep_libutil, dep_libei, dep_libevdev], + dependencies: [ + dep_libutil, + dep_libei, + dep_libevdev, + dep_libxkbcommon + ], include_directories: [inc_builddir], install: true, )