1#include <limits.h>
2#include <stdbool.h>
3#include <stdlib.h>
4#include <string.h>
5#include <sys/shm.h>
6#include <sys/ipc.h>
7
8#include <X11/Xlib.h>
9#include <X11/Xutil.h>
10#include <X11/keysym.h>
11#include <X11/cursorfont.h>
12#include <X11/extensions/XShm.h>
13
14#include "maus.h"
15#include "maus_input.h"
16#include "maus_x11.h"
17#include "utils.h"
18
19typedef struct {
20 KeySym x11;
21 MausKey maus;
22} KeyMapEntry;
23
24static bool fb_create(Maus* mw);
25static bool fb_create_shm(Maus* mw);
26/* TODO? static bool fb_create_ximage(Maus* mw); */
27static void fb_destroy(Maus* mw);
28static bool handle_event(XEvent* xev, MausEvent* ev, Maus* mw);
29static MausKey keysym_to_mauskey(KeySym sym);
30static MausMouseButton mouse_button_to_maus(int btn);
31static int xerr(Display* dpy, XErrorEvent* ev);
32
33static const KeyMapEntry keymap[] = {
34 { XK_BackSpace, MAUS_KEY_BACKSPACE }, { XK_Tab, MAUS_KEY_TAB },
35 { XK_Return, MAUS_KEY_ENTER }, { XK_Escape, MAUS_KEY_ESCAPE },
36 { XK_apostrophe, MAUS_KEY_APOSTROPHE },{ XK_comma, MAUS_KEY_COMMA },
37 { XK_space, MAUS_KEY_SPACE }, { XK_minus, MAUS_KEY_MINUS },
38 { XK_period, MAUS_KEY_PERIOD }, { XK_slash, MAUS_KEY_SLASH },
39 { XK_semicolon, MAUS_KEY_SEMICOLON }, { XK_equal, MAUS_KEY_EQUAL },
40 { XK_Delete, MAUS_KEY_DELETE }, { XK_Left, MAUS_KEY_LEFT },
41 { XK_Right, MAUS_KEY_RIGHT }, { XK_Up, MAUS_KEY_UP },
42 { XK_Down, MAUS_KEY_DOWN }, { XK_Home, MAUS_KEY_HOME },
43 { XK_End, MAUS_KEY_END }, { XK_Page_Up, MAUS_KEY_PAGE_UP },
44 { XK_Page_Down, MAUS_KEY_PAGE_DOWN }, { XK_Insert, MAUS_KEY_INSERT },
45 { XK_Shift_L, MAUS_KEY_SHIFT_L }, { XK_Shift_R, MAUS_KEY_SHIFT_R },
46 { XK_Control_L, MAUS_KEY_CONTROL_L }, { XK_Control_R, MAUS_KEY_CONTROL_R },
47 { XK_Alt_L, MAUS_KEY_ALT_L }, { XK_Alt_R, MAUS_KEY_ALT_R },
48 { XK_Super_L, MAUS_KEY_SUPER_L }, { XK_Super_R, MAUS_KEY_SUPER_R },
49 { XK_Caps_Lock, MAUS_KEY_CAPS_LOCK }, { XK_Num_Lock, MAUS_KEY_NUM_LOCK },
50 { XK_Pause, MAUS_KEY_PAUSE }, { XK_Menu, MAUS_KEY_MENU },
51 { XK_KP_Add, MAUS_KEY_KP_ADD }, { XK_KP_Enter, MAUS_KEY_KP_ENTER },
52 { XK_bracketright, MAUS_KEY_RIGHT_BRACKET }, { XK_grave, MAUS_KEY_GRAVE },
53 { XK_Scroll_Lock, MAUS_KEY_SCROLL_LOCK }, { XK_Print, MAUS_KEY_PRINT_SCREEN },
54 { XK_KP_Decimal, MAUS_KEY_KP_DECIMAL }, { XK_KP_Divide, MAUS_KEY_KP_DIVIDE },
55 { XK_KP_Multiply, MAUS_KEY_KP_MULTIPLY }, { XK_KP_Subtract, MAUS_KEY_KP_SUBTRACT },
56 { XK_bracketleft, MAUS_KEY_LEFT_BRACKET }, { XK_backslash, MAUS_KEY_BACKSLASH },
57 { XK_KP_Equal, MAUS_KEY_KP_EQUAL },
58};
59
60static int xerrored;
61
62static bool fb_create(Maus* mw)
63{
64 MausBackend* be = &mw->backend;
65 be->image = NULL;
66 be->shmat = false;
67 be->shm.shmid = -1;
68 be->shm.shmaddr = NULL;
69 mw->fb = NULL;
70 mw->stride = 0;
71
72 return fb_create_shm(mw);
73}
74
75/* allocate and store a new shm for the framebuffer */
76static bool fb_create_shm(Maus* mw)
77{
78 MausBackend* be = &mw->backend;
79 if (!XShmQueryExtension(be->display))
80 return false;
81
82 int scr = DefaultScreen(be->display);
83 int depth = DefaultDepth(be->display, scr);
84 Visual* vis = DefaultVisual(be->display, scr);
85 if (!vis) {
86 maus_log(stderr, "could not get default visual");
87 return false;
88 }
89
90 be->image = XShmCreateImage(
91 be->display, vis, depth, ZPixmap, NULL,
92 &be->shm, mw->width, mw->height
93 );
94 if (!be->image) {
95 maus_log(stderr, "could not create XImage (shm)");
96 return false;
97 }
98
99 if (be->image->bits_per_pixel != 32) {
100 XDestroyImage(be->image);
101 be->image = NULL;
102 maus_log(stderr, "XImage not 32bpp");
103 return false;
104 }
105
106 size_t size = be->image->bytes_per_line * be->image->height;
107 be->shm.shmid = shmget(IPC_PRIVATE, size, IPC_CREAT | 0600);
108 if (be->shm.shmid < 0) {
109 XDestroyImage(be->image);
110 be->image = NULL;
111 maus_log(stderr, "shmget() failed");
112 return false;
113 }
114
115 be->shm.shmaddr = shmat(be->shm.shmid, NULL, 0);
116 if (be->shm.shmaddr == (char*)-1) {
117 shmctl(be->shm.shmid, IPC_RMID, NULL);
118 XDestroyImage(be->image);
119 be->image = NULL;
120 maus_log(stderr, "shmat() failed");
121 return false;
122 }
123
124 be->shm.readOnly = False;
125 be->image->data = be->shm.shmaddr;
126
127 xerrored = 0;
128 int (*old_xerr)(Display*, XErrorEvent*) = XSetErrorHandler(xerr);
129
130 if (!XShmAttach(be->display, &be->shm))
131 xerrored = 1;
132
133 XSync(be->display, False);
134 XSetErrorHandler(old_xerr);
135 if (xerrored) {
136 shmdt(be->shm.shmaddr);
137 shmctl(be->shm.shmid, IPC_RMID, NULL);
138 XDestroyImage(be->image);
139
140 be->image = NULL;
141 mw->fb = NULL;
142 be->shm.shmaddr = NULL;
143 be->shm.shmid = -1;
144 maus_log(stderr, "XShmAttach failed");
145 return false;
146 }
147
148 shmctl(be->shm.shmid, IPC_RMID, NULL);
149
150 be->shmat = true;
151 mw->fb = (uint32_t*) be->image->data;
152 mw->stride = be->image->bytes_per_line / sizeof(uint32_t);
153
154 return true;
155}
156
157static void fb_destroy(Maus* mw)
158{
159 MausBackend* be = &mw->backend;
160 Display* dpy = be->display;
161 if (be->shmat) {
162 XShmDetach(dpy, &be->shm);
163 XSync(dpy, False);
164 be->shmat = false;
165 }
166
167 if (be->image) {
168 XDestroyImage(be->image);
169 be->image = NULL;
170 }
171
172 if (be->shm.shmaddr && be->shm.shmaddr != (char*) -1) {
173 shmdt(be->shm.shmaddr);
174 be->shm.shmaddr = NULL;
175 }
176
177 /* normally already removed after attach but
178 can check anyways */
179 if (be->shm.shmid >= 0) {
180 shmctl(be->shm.shmid, IPC_RMID, NULL);
181 be->shm.shmid = -1;
182 }
183
184 mw->fb = NULL;
185 mw->stride = 0;
186}
187
188static bool handle_event(XEvent* xev, MausEvent* ev, Maus* mw)
189{
190 MausBackend* be = &mw->backend;
191 uint32_t code = 0;
192 unsigned int mb;
193 MausMouseButton mbtype;
194
195 switch (xev->type) {
196 case Expose:
197 if (xev->xexpose.count != 0)
198 return false;
199 ev->type = MAUS_EV_REDRAW;
200 return true;
201
202 case ClientMessage:
203 if ((Atom)xev->xclient.data.l[0] == be->atoms[MAUS_ATOM_WM_DELETE_WINDOW]) {
204 ev->type = MAUS_EV_CLOSE;
205 return true;
206 }
207 break;
208
209 case MappingNotify:
210 XRefreshKeyboardMapping(&xev->xmapping);
211 return true;
212 break;
213
214 case KeyPress: {
215 ev->type = MAUS_EV_KEY;
216 code = xev->xkey.keycode;
217 ev->key.code = code;
218 ev->key.pressed = true;
219 if (code < MAUS_KEYCODE_LAST)
220 mw->key_codes[code] = true;
221
222 /* grab the raw, base sym */
223 KeySym sym = XLookupKeysym(&xev->xkey, 0);
224 ev->key.key = keysym_to_mauskey(sym);
225 if (ev->key.key != MAUS_KEY_NONE)
226 mw->key_syms[ev->key.key] = true;
227
228 char buf[8] = {0};
229 int len = XLookupString(&xev->xkey, buf, sizeof(buf), NULL, NULL);
230 if (len > 0)
231 ev->key.text = (buf[0] == '\r') ? '\n' : buf[0];
232 else
233 ev->key.text = 0;
234
235 return true;
236 }
237
238 case KeyRelease: {
239 ev->type = MAUS_EV_KEY;
240 code = xev->xkey.keycode;
241 ev->key.code = code;
242 ev->key.pressed = false;
243 ev->key.text = 0;
244
245 if (code < MAUS_KEYCODE_LAST)
246 mw->key_codes[code] = false;
247
248 /* unset base sym set on `KeyPress` */
249 KeySym sym = XLookupKeysym(&xev->xkey, 0);
250 ev->key.key = keysym_to_mauskey(sym);
251 if (ev->key.key != MAUS_KEY_NONE)
252 mw->key_syms[ev->key.key] = false;
253
254 return true;
255 }
256
257 case ButtonPress:
258 ev->type = MAUS_EV_MOUSE_BUTTON;
259 mb = xev->xbutton.button;
260 mbtype = mouse_button_to_maus(mb);
261 ev->mouse.button.button = mbtype;
262 ev->mouse.button.pressed = true;
263 mw->mouse_buttons[mbtype] = true;
264 return true;
265
266 case ButtonRelease:
267 ev->type = MAUS_EV_MOUSE_BUTTON;
268 mb = xev->xbutton.button;
269 mbtype = mouse_button_to_maus(mb);
270 ev->mouse.button.button = mbtype;
271 ev->mouse.button.pressed = false;
272 mw->mouse_buttons[mbtype] = false;
273 return true;
274
275 case MotionNotify:
276 ev->type = MAUS_EV_MOUSE_MOTION;
277 ev->mouse.motion.x = xev->xmotion.x;
278 ev->mouse.motion.y = xev->xmotion.y;
279 return true;
280
281 case ConfigureNotify:
282 ev->type = MAUS_EV_RESIZE;
283 ev->resize.width = xev->xconfigure.width;
284 ev->resize.height = xev->xconfigure.height;
285 return true;
286
287 case SelectionRequest: {
288 XSelectionRequestEvent* req = &xev->xselectionrequest;
289 XEvent rsp; /* response */
290
291 Atom utf8_string = XInternAtom(be->display, "UTF8_STRING", False);
292 Atom clipboard = XInternAtom(be->display, "CLIPBOARD", False);
293
294 rsp.type = SelectionNotify;
295 rsp.xselection.display = req->display;
296 rsp.xselection.requestor = req->requestor;
297 rsp.xselection.selection = req->selection;
298 rsp.xselection.target = req->target;
299 rsp.xselection.property = None;
300 rsp.xselection.time = req->time;
301
302 /* assign clipboard text to requesting programs
303 window property */
304 if (req->selection == clipboard &&
305 req->target == utf8_string &&
306 mw->clipboard) {
307 XChangeProperty(
308 req->display, req->requestor,
309 req->property, utf8_string, 8,
310 PropModeReplace, (unsigned char*)mw->clipboard,
311 strlen(mw->clipboard)
312 );
313 rsp.xselection.property = req->property;
314 }
315
316 /* send reply back to the prog requesting paste */
317 XSendEvent(req->display, req->requestor, False, 0, &rsp);
318 XFlush(be->display);
319
320 return false; /* skip maus polling it */
321 }
322 }
323
324 return false;
325}
326
327static MausKey keysym_to_mauskey(KeySym sym)
328{
329 if (sym >= 0x20 && sym <= 0x7E)
330 return (MausKey)sym;
331
332 if (sym >= XK_F1 && sym <= XK_F12)
333 return MAUS_KEY_F1 + (sym - XK_F1);
334 if (sym >= XK_KP_0 && sym <= XK_KP_9)
335 return MAUS_KEY_KP_0 + (sym - XK_KP_0);
336
337 for (size_t i = 0; i < sizeof(keymap)/ sizeof(keymap[0]); i++) {
338 if (keymap[i].x11 == sym)
339 return keymap[i].maus;
340 }
341
342 return MAUS_KEY_NONE;
343}
344
345static MausMouseButton mouse_button_to_maus(int btn)
346{
347 switch (btn) {
348 case Button1: return MAUS_MOUSE_BUTTON_LEFT;
349 case Button2: return MAUS_MOUSE_BUTTON_MIDDLE;
350 case Button3: return MAUS_MOUSE_BUTTON_RIGHT;
351 case Button4: return MAUS_MOUSE_BUTTON_SCROLL_UP;
352 case Button5: return MAUS_MOUSE_BUTTON_SCROLL_DOWN;
353 default: return MAUS_MOUSE_BUTTON_NONE;
354 }
355}
356
357static int xerr(Display* dpy, XErrorEvent* ev)
358{
359 (void) dpy;
360 (void) ev;
361 xerrored = 1;
362 return 0;
363}
364
365void maus_clipboard_set_text(Maus* mw, const char* text)
366{
367 MausBackend* be = &mw->backend;
368 if (!be->display || be->win == None)
369 return;
370
371 /* clear old entry */
372 if (mw->clipboard) {
373 free(mw->clipboard);
374 mw->clipboard = NULL;
375 }
376
377 if (text)
378 mw->clipboard = maus_strdup(text);
379
380 Atom clipboard = XInternAtom(be->display, "CLIPBOARD", False);
381 XSetSelectionOwner(be->display, clipboard, be->win, CurrentTime);
382 XFlush(be->display);
383}
384
385char* maus_clipboard_get_text(Maus* mw)
386{
387 MausBackend* be = &mw->backend;
388 Atom clipboard = XInternAtom(be->display, "CLIPBOARD", False);
389 Atom utf8_string = XInternAtom(be->display, "UTF8_STRING", False);
390 Atom maus_clipboard_prop = XInternAtom(be->display, "MAUS_CLIPBOARD_PROP", False);
391
392 /* fetch clipboard data to `maus_clipboard_prop`
393 window property */
394 XConvertSelection(
395 be->display, clipboard, utf8_string,
396 maus_clipboard_prop, be->win, CurrentTime
397 );
398 XFlush(be->display);
399
400 /* check for selection response */
401 XEvent xev;
402 bool ok = false;
403 for (int timeout = 0; timeout < 10000; timeout++) {
404 if (XCheckTypedWindowEvent(be->display, be->win, SelectionNotify, &xev)) {
405 if (xev.xselection.property != None)
406 ok = true;
407 break;
408 }
409 }
410 if (!ok)
411 return NULL;
412
413 /* read string off property */
414 Atom actual_type;
415 int actual_fmt;
416 unsigned long nitems;
417 unsigned long bytes_after;
418 unsigned char* prop_data = NULL;
419 XGetWindowProperty(
420 be->display, be->win, maus_clipboard_prop, 0,LONG_MAX,
421 True, utf8_string, &actual_type, &actual_fmt, &nitems,
422 &bytes_after, &prop_data
423 );
424
425 char* res = NULL;
426 if (prop_data) {
427 res = maus_strdup((char*)prop_data);
428 XFree(prop_data);
429 }
430
431 return res;
432}
433
434void maus_close(Maus* mw)
435{
436 MausBackend* be = &mw->backend;
437 fb_destroy(mw);
438 if (mw->bfb) {
439 free(mw->bfb);
440 mw->bfb = NULL;
441 }
442 if (mw->clipboard) {
443 free(mw->clipboard);
444 mw->clipboard = NULL;
445 }
446 if (be->win != None) {
447 (void) maus_close_window(mw);
448 }
449 if (be->display)
450 XCloseDisplay(be->display);
451}
452
453bool maus_close_window(Maus* mw)
454{
455 MausBackend* be = &mw->backend;
456 if (be->gc) {
457 XFreeGC(be->display, be->gc);
458 be->gc = 0;
459 }
460
461 if (be->win != None) {
462 XUnmapWindow(be->display, be->win);
463 XDestroyWindow(be->display, be->win);
464 }
465
466 return true;
467}
468
469bool maus_create_window(Maus* mw)
470{
471 MausBackend* be = &mw->backend;
472 Display* dpy = be->display;
473 Window* win = &be->win;
474
475 *win = XCreateSimpleWindow(
476 dpy, be->root,
477 mw->x, mw->y,
478 mw->width, mw->height,
479 0u, 0u, 0u
480 );
481
482 if (*win == None)
483 return false;
484
485 XSelectInput(dpy, *win,
486 ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask |
487 ButtonReleaseMask | PointerMotionMask | StructureNotifyMask
488 );
489 /* no black flashing when resizing */
490 XSetWindowBackgroundPixmap(dpy, *win, None);
491
492 /* atoms */
493 be->atoms[MAUS_ATOM_WM_DELETE_WINDOW] = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
494
495 XSetWMProtocols(dpy, *win, &be->atoms[MAUS_ATOM_WM_DELETE_WINDOW], 1);
496 XStoreName(dpy, *win, mw->title);
497
498 XMapWindow(dpy, *win);
499 XFlush(dpy);
500
501 be->gc = XCreateGC(dpy, *win, 0, NULL);
502 if (!be->gc) {
503 maus_log(stderr, "failed to create window GC");
504 maus_close_window(mw);
505 return false;
506 }
507
508 return true;
509}
510
511void maus_clear(Maus* mw, MausColor col)
512{
513 uint32_t col_up = MAUS_UNPACK_COL(col);
514 uint32_t pxs = mw->height * mw->width;
515 for (uint32_t i = 0; i < pxs; i++)
516 mw->bfb[i] = col_up;
517}
518
519Maus* maus_init(const char* title, int x, int y, int width, int height)
520{
521 if (MAUS_WARN_BACKEND_AUTO_SEL)
522 maus_log(stderr, "backend auto-selected: X11");
523
524 Display* d = XOpenDisplay(NULL);
525 if (!d) {
526 maus_die("failed to open X11 display");
527 return NULL;
528 }
529
530 Maus* mw = calloc(1, sizeof(Maus));
531 MausBackend* be = &mw->backend;
532 be->display = d;
533 be->root = DefaultRootWindow(d);
534 be->win = None;
535 mw->frame_time_last = maus_get_time_ns();
536 mw->title = title;
537 mw->width = width;
538 mw->height = height;
539 mw->x = x;
540 mw->y = y;
541 mw->fb = NULL;
542
543 be->gc = 0;
544 be->image = NULL;
545 be->shmat = false;
546 be->shm.shmid = -1;
547 be->shm.shmaddr = NULL;
548 if (!fb_create(mw)) {
549 maus_log(stderr, "failed to create framebuffer");
550 maus_close(mw);
551 free(mw);
552 return NULL;
553 }
554 mw->bfb = calloc(mw->stride * mw->height, sizeof(uint32_t));
555 if (!mw->bfb) {
556 maus_log(stderr, "failed to allocate back buffer");
557 maus_close(mw);
558 free(mw);
559 return NULL;
560 }
561
562 return mw;
563}
564
565bool maus_event_poll(Maus* mw, MausEvent* ev)
566{
567 MausBackend* be = &mw->backend;
568 XEvent xev;
569 while (XPending(be->display)) {
570 XNextEvent(be->display, &xev);
571 ev->type = MAUS_EV_NONE;
572 if (handle_event(&xev, ev, mw))
573 return true;
574
575 }
576
577 return false;
578}
579
580void maus_event_wait(Maus* mw, MausEvent* ev)
581{
582 MausBackend* be = &mw->backend;
583 XEvent xev;
584 for (;;) {
585 XNextEvent(be->display, &xev);
586 ev->type = MAUS_EV_NONE;
587 if (handle_event(&xev, ev, mw))
588 return;
589 }
590}
591
592void maus_present(Maus* mw)
593{
594 MausBackend* be = &mw->backend;
595 if (!be->image || be->win == None)
596 return;
597
598 uint32_t bytes = mw->stride * mw->height * sizeof(uint32_t);
599 memcpy(mw->fb, mw->bfb, bytes);
600
601 XShmPutImage(
602 be->display, be->win, be->gc, be->image,
603 0, 0, 0, 0, mw->width, mw->height, False
604 );
605
606 XFlush(be->display);
607}
608
609bool maus_resize(Maus* mw, uint32_t width, uint32_t height)
610{
611 MausBackend* be = &mw->backend;
612 if (width == 0 || height == 0 ||
613 (mw->width == width && mw->height == height))
614 return false;
615
616 fb_destroy(mw);
617 if (mw->bfb) {
618 free(mw->bfb);
619 mw->bfb = NULL;
620 }
621
622 mw->width = width;
623 mw->height = height;
624
625 XSync(be->display, False);
626 XFlush(be->display);
627
628 if (!fb_create(mw))
629 return false;
630
631 mw->bfb = calloc(mw->stride * mw->height, sizeof(uint32_t));
632 if (!mw->bfb)
633 return false;
634
635 return true;
636}
637
638void maus_cur_set_mode(Maus* mw, MausCursorState state)
639{
640 MausBackend* be = &mw->backend;
641 Display* dpy = be->display;
642 Window win = be->win;
643
644 if (!dpy) {
645 maus_log(stderr, "display is NULL");
646 return;
647 }
648 if (win == None) {
649 maus_log(stderr, "win is None");
650 return;
651 }
652
653 /* show cursor */
654 if (state == MAUS_CURSOR_STATE_VISIBLE) {
655 XUndefineCursor(dpy, win);
656 XFlush(dpy);
657 return;
658 }
659
660 /* hide cursor */
661 if (state == MAUS_CURSOR_STATE_HIDDEN) {
662 Pixmap blank_pm = XCreatePixmap(dpy, win, 1, 1, 1);
663 XColor black = {.red=0, .green=0, .blue=0};
664 Cursor blank_cur = XCreatePixmapCursor(
665 dpy, blank_pm, blank_pm, &black, &black, 0, 0
666 );
667 XFreePixmap(dpy, blank_pm);
668
669 XDefineCursor(dpy, win, blank_cur);
670 XFreeCursor(dpy, blank_cur);
671 XFlush(dpy);
672 return;
673 }
674
675 /* lock cursor */
676 if (state == MAUS_CURSOR_STATE_LOCKED) {
677 Mask mask = ButtonPressMask | ButtonReleaseMask | PointerMotionMask;
678 int grab = XGrabPointer(
679 dpy, win, True, mask, GrabModeAsync,
680 GrabModeAsync, win, None, CurrentTime
681 );
682
683 if (grab != GrabSuccess) {
684 maus_log(stderr, "failed to grab mouse pointer");
685 return;
686 }
687
688 /* TODO keep position just snap to window dimensions */
689 XWarpPointer(dpy, None, win, 0, 0, 0, 0, mw->width/2, mw->height/2);
690 XFlush(dpy);
691 return;
692 }
693
694 /* unlock cursor */
695 if (state == MAUS_CURSOR_STATE_FREE) {
696 XUngrabPointer(dpy, CurrentTime);
697 XFlush(dpy);
698 return;
699 }
700}
701