1 #define _POSIX_C_SOURCE 200809L
2 #include <GLES2/gl2.h>
3 #include <stdio.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <unistd.h>
7 #include <wayland-client.h>
8 #include <wayland-egl.h>
9 #include <wlr/render/egl.h>
10 #include "text-input-unstable-v3-client-protocol.h"
11 #include "xdg-shell-client-protocol.h"
12
13 const char usage[] = "Usage: text-input [seconds [width height]]\n\
14 \n\
15 Creates a xdg-toplevel using the text-input protocol.\n\
16 It will be solid black when it has no text input focus, yellow when it\n\
17 has focus, and red when it was notified that the focus moved away\n\
18 but still didn't give up the text input ability.\n\
19 \n\
20 The \"seconds\" argument is optional and defines the delay between getting\n\
21 notified of lost focus and releasing text input.\n\
22 \n\
23 The \"width\" and \"height\" arguments define the window shape.\n\
24 \n\
25 The console will print the internal state of the text field:\n\
26 - the text in the 1st line\n\
27 - \".\" under each preedit character\n\
28 - \"_\" under each selected preedit character\n\
29 - \"|\" at the cursor position if there are no selected characters in the\n\
30 preedit.\n\
31 \n\
32 The cursor positions may be inaccurate, especially in presence of zero-width\n\
33 characters or non-monospaced fonts.\n";
34
35 struct text_input_state {
36 char *commit;
37 struct {
38 char *text;
39 int32_t cursor_begin;
40 int32_t cursor_end;
41 } preedit;
42 struct {
43 uint32_t after_length;
44 uint32_t before_length;
45 } delete_surrounding;
46 };
47
48 static struct text_input_state pending = {0};
49 static struct text_input_state current = {0};
50 static bool entered = false;
51 static uint32_t serial;
52 static char *buffer; // text buffer
53 // cursor is not present, there's no way to move it outside of preedit
54
55 static int sleeptime = 0;
56 static int width = 100, height = 200;
57 static int enabled = 0;
58
59 static struct wl_display *display = NULL;
60 static struct wl_compositor *compositor = NULL;
61 static struct wl_seat *seat = NULL;
62 static struct xdg_wm_base *wm_base = NULL;
63 static struct zwp_text_input_manager_v3 *text_input_manager = NULL;
64 static struct zwp_text_input_v3 *text_input = NULL;
65
66 struct wlr_egl egl;
67 struct wl_egl_window *egl_window;
68 struct wlr_egl_surface *egl_surface;
69
draw(void)70 static void draw(void) {
71 eglMakeCurrent(egl.display, egl_surface, egl_surface, egl.context);
72
73 float color[] = {1.0, 1.0, 0.0, 1.0};
74 color[0] = enabled * 1.0;
75 color[1] = entered * 1.0;
76
77 glViewport(0, 0, width, height);
78 glClearColor(color[0], color[1], color[2], 1.0);
79 glClear(GL_COLOR_BUFFER_BIT);
80
81 eglSwapBuffers(egl.display, egl_surface);
82 }
83
utf8_strlen(char * str)84 static size_t utf8_strlen(char *str) {
85 size_t cp_count = 0;
86 for (; *str != '\0'; str++) {
87 if ((*str & 0xc0) != 0x80) {
88 cp_count++;
89 }
90 }
91 return cp_count;
92 }
93
utf8_offset(char * utf8_str,size_t byte_offset)94 static size_t utf8_offset(char *utf8_str, size_t byte_offset) {
95 size_t cp_count = 0;
96 for (char *c = utf8_str; c < utf8_str + byte_offset; c++) {
97 if ((*c & 0xc0) != 0x80) {
98 cp_count++;
99 }
100 }
101 return cp_count;
102 }
103
104 // TODO: would be nicer to have this text display inside the window
show_status(void)105 static void show_status(void) {
106 printf("State %d:", serial);
107 if (!enabled) {
108 printf(" disabled");
109 }
110
111 char *preedit_text = current.preedit.text;
112 if (!preedit_text) {
113 preedit_text = "";
114 }
115
116 printf("\n");
117 printf("%s", buffer);
118 printf("%s\n", preedit_text);
119
120 // Positioning of the cursor requires UTF8 offsets to match monospaced
121 // glyphs
122 for (unsigned i = 0; i < utf8_strlen(buffer); i++) {
123 printf(" ");
124 }
125 char *cursor_mark = calloc(utf8_strlen(preedit_text) + 2, sizeof(char));
126 for (unsigned i = 0; i < utf8_strlen(preedit_text); i++) {
127 cursor_mark[i] = '.';
128 }
129 if (current.preedit.cursor_begin == -1
130 && current.preedit.cursor_end == -1) {
131 goto end;
132 }
133 if (current.preedit.cursor_begin == -1
134 || current.preedit.cursor_end == -1) {
135 printf("Only one cursor side is defined: %d to %d\n",
136 current.preedit.cursor_begin, current.preedit.cursor_end);
137 goto end;
138 }
139
140 if ((unsigned)current.preedit.cursor_begin > strlen(preedit_text)) {
141 printf("Cursor out of bounds\n");
142 goto end;
143 }
144
145 if (current.preedit.cursor_begin == current.preedit.cursor_end) {
146 cursor_mark[utf8_offset(preedit_text, current.preedit.cursor_begin)]
147 = '|';
148 goto print;
149 }
150
151 if (current.preedit.cursor_begin > current.preedit.cursor_end) {
152 printf("End cursor is before start cursor\n");
153 goto end;
154 }
155
156 // negative offsets already checked before
157 for (unsigned i = utf8_offset(preedit_text, current.preedit.cursor_begin);
158 i < utf8_offset(preedit_text, current.preedit.cursor_end); i++) {
159 cursor_mark[i] = '_';
160 }
161 print:
162 printf("%s\n", cursor_mark);
163 end:
164 free(cursor_mark);
165 }
166
commit(struct zwp_text_input_v3 * text_input)167 static void commit(struct zwp_text_input_v3 *text_input) {
168 zwp_text_input_v3_commit(text_input);
169 serial++;
170 }
171
send_status_update(struct zwp_text_input_v3 * text_input)172 static void send_status_update(struct zwp_text_input_v3 *text_input) {
173 zwp_text_input_v3_set_surrounding_text(text_input, buffer, strlen(buffer), strlen(buffer));
174 zwp_text_input_v3_set_text_change_cause(text_input, ZWP_TEXT_INPUT_V3_CHANGE_CAUSE_INPUT_METHOD);
175 commit(text_input);
176 }
177
text_input_handle_enter(void * data,struct zwp_text_input_v3 * zwp_text_input_v3,struct wl_surface * surface)178 static void text_input_handle_enter(void *data,
179 struct zwp_text_input_v3 *zwp_text_input_v3,
180 struct wl_surface *surface) {
181 entered = true;
182 zwp_text_input_v3_enable(zwp_text_input_v3);
183 commit(zwp_text_input_v3);
184 enabled = true;
185 draw();
186 show_status();
187 }
188
text_input_handle_leave(void * data,struct zwp_text_input_v3 * zwp_text_input_v3,struct wl_surface * surface)189 static void text_input_handle_leave(void *data,
190 struct zwp_text_input_v3 *zwp_text_input_v3,
191 struct wl_surface *surface) {
192 entered = false;
193 draw();
194 wl_display_roundtrip(display);
195 sleep(sleeptime);
196 zwp_text_input_v3_disable(zwp_text_input_v3);
197 commit(zwp_text_input_v3);
198 enabled = false;
199 draw();
200 show_status();
201 }
202
text_input_commit_string(void * data,struct zwp_text_input_v3 * zwp_text_input_v3,const char * text)203 static void text_input_commit_string(void *data,
204 struct zwp_text_input_v3 *zwp_text_input_v3,
205 const char *text) {
206 free(pending.commit);
207 pending.commit = strdup(text);
208 }
209
text_input_delete_surrounding_text(void * data,struct zwp_text_input_v3 * zwp_text_input_v3,uint32_t before_length,uint32_t after_length)210 static void text_input_delete_surrounding_text(void *data,
211 struct zwp_text_input_v3 *zwp_text_input_v3,
212 uint32_t before_length, uint32_t after_length) {
213 pending.delete_surrounding.before_length = before_length;
214 pending.delete_surrounding.after_length = after_length;
215 }
216
text_input_preedit_string(void * data,struct zwp_text_input_v3 * zwp_text_input_v3,const char * text,int32_t cursor_begin,int32_t cursor_end)217 static void text_input_preedit_string(void *data,
218 struct zwp_text_input_v3 *zwp_text_input_v3,
219 const char *text, int32_t cursor_begin, int32_t cursor_end) {
220 free(pending.preedit.text);
221 pending.preedit.text = strdup(text);
222 pending.preedit.cursor_begin = cursor_begin;
223 pending.preedit.cursor_end = cursor_end;
224 }
225
text_input_handle_done(void * data,struct zwp_text_input_v3 * zwp_text_input_v3,uint32_t incoming_serial)226 static void text_input_handle_done(void *data,
227 struct zwp_text_input_v3 *zwp_text_input_v3,
228 uint32_t incoming_serial) {
229 if (serial != incoming_serial) {
230 fprintf(stderr, "Received serial %d while expecting %d\n", incoming_serial, serial);
231 return;
232 }
233 free(current.preedit.text);
234 free(current.commit);
235 current = pending;
236 struct text_input_state empty = {0};
237 pending = empty;
238
239 if (current.delete_surrounding.after_length + current.delete_surrounding.before_length > 0) {
240 // cursor is always after committed text, after_length != 0 will never happen
241 unsigned delete_before = current.delete_surrounding.before_length;
242 if (delete_before > strlen(buffer)) {
243 delete_before = strlen(buffer);
244 }
245 buffer[strlen(buffer) - delete_before] = '\0';
246 }
247
248 char *commit_string = current.commit;
249 if (!commit_string) {
250 commit_string = "";
251 }
252 char *old_buffer = buffer;
253 buffer = calloc(strlen(buffer) + strlen(commit_string) + 1, sizeof(char)); // realloc may fail anyway
254 strcpy(buffer, old_buffer);
255 free(old_buffer);
256 strcat(buffer, commit_string);
257
258 send_status_update(zwp_text_input_v3);
259 show_status();
260 }
261
262 static const struct zwp_text_input_v3_listener text_input_listener = {
263 .enter = text_input_handle_enter,
264 .leave = text_input_handle_leave,
265 .commit_string = text_input_commit_string,
266 .delete_surrounding_text = text_input_delete_surrounding_text,
267 .preedit_string = text_input_preedit_string,
268 .done = text_input_handle_done,
269 };
270
xdg_surface_handle_configure(void * data,struct xdg_surface * xdg_surface,uint32_t serial)271 static void xdg_surface_handle_configure(void *data,
272 struct xdg_surface *xdg_surface, uint32_t serial) {
273 xdg_surface_ack_configure(xdg_surface, serial);
274 wl_egl_window_resize(egl_window, width, height, 0, 0);
275 draw();
276 }
277
278 static const struct xdg_surface_listener xdg_surface_listener = {
279 .configure = xdg_surface_handle_configure,
280 };
281
xdg_toplevel_handle_configure(void * data,struct xdg_toplevel * xdg_toplevel,int32_t w,int32_t h,struct wl_array * states)282 static void xdg_toplevel_handle_configure(void *data,
283 struct xdg_toplevel *xdg_toplevel, int32_t w, int32_t h,
284 struct wl_array *states) {
285 width = w;
286 height = h;
287 }
288
xdg_toplevel_handle_close(void * data,struct xdg_toplevel * xdg_toplevel)289 static void xdg_toplevel_handle_close(void *data,
290 struct xdg_toplevel *xdg_toplevel) {
291 exit(EXIT_SUCCESS);
292 }
293
294 static const struct xdg_toplevel_listener xdg_toplevel_listener = {
295 .configure = xdg_toplevel_handle_configure,
296 .close = xdg_toplevel_handle_close,
297 };
298
handle_global(void * data,struct wl_registry * registry,uint32_t name,const char * interface,uint32_t version)299 static void handle_global(void *data, struct wl_registry *registry,
300 uint32_t name, const char *interface, uint32_t version) {
301 if (strcmp(interface, "wl_compositor") == 0) {
302 compositor = wl_registry_bind(registry, name,
303 &wl_compositor_interface, 1);
304 } else if (strcmp(interface, xdg_wm_base_interface.name) == 0) {
305 wm_base = wl_registry_bind(registry, name, &xdg_wm_base_interface, 1);
306 } else if (strcmp(interface, zwp_text_input_manager_v3_interface.name) == 0) {
307 text_input_manager = wl_registry_bind(registry, name,
308 &zwp_text_input_manager_v3_interface, 1);
309 } else if (strcmp(interface, wl_seat_interface.name) == 0) {
310 seat = wl_registry_bind(registry, name, &wl_seat_interface, version);
311 }
312 }
313
handle_global_remove(void * data,struct wl_registry * registry,uint32_t name)314 static void handle_global_remove(void *data, struct wl_registry *registry,
315 uint32_t name) {
316 // who cares
317 }
318
319 static const struct wl_registry_listener registry_listener = {
320 .global = handle_global,
321 .global_remove = handle_global_remove,
322 };
323
main(int argc,char ** argv)324 int main(int argc, char **argv) {
325 if (argc > 1) {
326 if (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0) {
327 printf(usage);
328 return 0;
329 }
330 sleeptime = atoi(argv[1]);
331 if (argc > 3) {
332 width = atoi(argv[2]);
333 height = atoi(argv[3]);
334 }
335 }
336
337 buffer = calloc(1, sizeof(char));
338
339 display = wl_display_connect(NULL);
340 if (display == NULL) {
341 fprintf(stderr, "Failed to create display\n");
342 return EXIT_FAILURE;
343 }
344
345 struct wl_registry *registry = wl_display_get_registry(display);
346 wl_registry_add_listener(registry, ®istry_listener, NULL);
347 wl_display_roundtrip(display);
348
349 if (compositor == NULL) {
350 fprintf(stderr, "wl-compositor not available\n");
351 return EXIT_FAILURE;
352 }
353 if (wm_base == NULL) {
354 fprintf(stderr, "xdg-shell not available\n");
355 return EXIT_FAILURE;
356 }
357 if (text_input_manager == NULL) {
358 fprintf(stderr, "text-input not available\n");
359 return EXIT_FAILURE;
360 }
361
362 text_input = zwp_text_input_manager_v3_get_text_input(text_input_manager, seat);
363
364 zwp_text_input_v3_add_listener(text_input, &text_input_listener, NULL);
365
366
367 wlr_egl_init(&egl, EGL_PLATFORM_WAYLAND_EXT, display, NULL,
368 WL_SHM_FORMAT_ARGB8888);
369
370 struct wl_surface *surface = wl_compositor_create_surface(compositor);
371 struct xdg_surface *xdg_surface =
372 xdg_wm_base_get_xdg_surface(wm_base, surface);
373 struct xdg_toplevel *xdg_toplevel = xdg_surface_get_toplevel(xdg_surface);
374
375 xdg_surface_add_listener(xdg_surface, &xdg_surface_listener, NULL);
376 xdg_toplevel_add_listener(xdg_toplevel, &xdg_toplevel_listener, NULL);
377
378 wl_surface_commit(surface);
379
380 egl_window = wl_egl_window_create(surface, width, height);
381 egl_surface = wlr_egl_create_surface(&egl, egl_window);
382
383 wl_display_roundtrip(display);
384
385 draw();
386
387 while (wl_display_dispatch(display) != -1) {
388 // This space intentionally left blank
389 }
390
391 return EXIT_SUCCESS;
392 }
393