graphics_xlib.c - ltk - GUI toolkit for X11 (WIP)
HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
HTML git clone https://lumidify.org/ltk.git (encrypted, but very slow)
HTML git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
graphics_xlib.c (16992B)
---
1 /*
2 * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
3 *
4 * Permission to use, copy, modify, and/or distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17 #include <stdio.h>
18 #include <string.h>
19
20 #include <X11/XKBlib.h>
21 #include <X11/Xlib.h>
22 #include <X11/Xutil.h>
23 #include <X11/extensions/XKB.h>
24 #include <X11/extensions/Xdbe.h>
25 #include <X11/extensions/dbe.h>
26
27 #if USE_XRANDR
28 #include <X11/extensions/Xrandr.h>
29 #endif
30
31 #include "graphics_xlib.h"
32 #include "color.h"
33 #include "memory.h"
34 #include "rect.h"
35 #include "util.h"
36
37 /* FIXME: kind of ugly to have this duplicated here and in event_xlib.c,
38 but I don't really want to give these functions linkage */
39 LTK_ARRAY_INIT_FUNC_DECL_STATIC(moninfo, struct ltk_moninfo)
40 LTK_ARRAY_INIT_IMPL_STATIC(moninfo, struct ltk_moninfo)
41
42 ltk_surface *
43 ltk_surface_create(ltk_renderwindow *window, int w, int h) {
44 ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
45 if (w <= 0)
46 w = 1;
47 if (h <= 0)
48 h = 1;
49 s->w = w;
50 s->h = h;
51 s->window = window;
52 s->d = XCreatePixmap(window->renderdata->dpy, window->xwindow, w, h, window->renderdata->depth);
53 #if USE_XFT == 1
54 s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
55 #endif
56 s->resizable = 1;
57 return s;
58 }
59
60 ltk_surface *
61 ltk_surface_from_window(ltk_renderwindow *window, int w, int h) {
62 ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
63 s->w = w;
64 s->h = h;
65 s->window = window;
66 s->d = window->drawable;
67 #if USE_XFT == 1
68 s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
69 #endif
70 s->resizable = 0;
71 return s;
72 }
73
74 void
75 ltk_surface_destroy(ltk_surface *s) {
76 #if USE_XFT == 1
77 XftDrawDestroy(s->xftdraw);
78 #endif
79 if (s->resizable)
80 XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
81 ltk_free(s);
82 }
83
84 void
85 ltk_surface_update_size(ltk_surface *s, int w, int h) {
86 /* FIXME: maybe return directly if surface is resizable? */
87 s->w = w;
88 s->h = h;
89 /* FIXME: sort of hacky (this is only used by window surface) */
90 #if USE_XFT == 1
91 XftDrawChange(s->xftdraw, s->d);
92 #endif
93 }
94
95 int
96 ltk_surface_resize(ltk_surface *s, int w, int h) {
97 if (!s->resizable)
98 return 1;
99 s->w = w;
100 s->h = h;
101 XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
102 s->d = XCreatePixmap(s->window->renderdata->dpy, s->window->xwindow, w, h, s->window->renderdata->depth);
103 #if USE_XFT == 1
104 XftDrawChange(s->xftdraw, s->d);
105 #endif
106 return 0;
107 }
108
109 void
110 ltk_surface_get_size(ltk_surface *s, int *w, int *h) {
111 *w = s->w;
112 *h = s->h;
113 }
114
115 void
116 ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width) {
117 XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
118 XSetLineAttributes(s->window->renderdata->dpy, s->window->gc, line_width, LineSolid, CapButt, JoinMiter);
119 XDrawRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
120 }
121
122 void
123 ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides) {
124 /* drawn as rectangles to have proper control over line width - I'm not sure how exactly
125 XDrawLine handles even line widths (i.e. on which side the extra pixel will be) */
126 XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
127 if (border_sides & LTK_BORDER_TOP)
128 XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, line_width);
129 if (border_sides & LTK_BORDER_BOTTOM)
130 XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y + rect.h - line_width, rect.w, line_width);
131 if (border_sides & LTK_BORDER_LEFT)
132 XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, line_width, rect.h);
133 if (border_sides & LTK_BORDER_RIGHT)
134 XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x + rect.w - line_width, rect.y, line_width, rect.h);
135 }
136
137 void
138 ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides, ltk_rect clip) {
139 if (line_width <= 0)
140 return;
141 /* NOTE: XRectangle only uses short, so this could cause issues */
142 XRectangle xclip = {clip.x, clip.y, clip.w, clip.h};
143 XSetClipRectangles(s->window->renderdata->dpy, s->window->gc, 0, 0, &xclip, 1, Unsorted);
144 ltk_surface_draw_border(s, c, rect, line_width, border_sides);
145 XSetClipMask(s->window->renderdata->dpy, s->window->gc, None);
146 }
147
148 void
149 ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
150 XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
151 XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
152 }
153
154 void
155 ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints) {
156 /* FIXME: maybe make this static since this won't be threaded anyways? */
157 XPoint tmp_points[6]; /* to avoid extra allocations when not necessary */
158 /* FIXME: this is ugly and inefficient */
159 XPoint *final_points;
160 if (npoints <= 6) {
161 final_points = tmp_points;
162 } else {
163 final_points = ltk_reallocarray(NULL, npoints, sizeof(XPoint));
164 }
165 /* FIXME: how to deal with ints that don't fit in short? */
166 for (size_t i = 0; i < npoints; i++) {
167 final_points[i].x = (short)points[i].x;
168 final_points[i].y = (short)points[i].y;
169 }
170 XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
171 XFillPolygon(s->window->renderdata->dpy, s->d, s->window->gc, final_points, (int)npoints, Complex, CoordModeOrigin);
172 if (npoints > 6)
173 ltk_free(final_points);
174 }
175
176 void
177 ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip) {
178 /* NOTE: XRectangle only uses short, so this could cause issues */
179 XRectangle xclip = {clip.x, clip.y, clip.w, clip.h};
180 XSetClipRectangles(s->window->renderdata->dpy, s->window->gc, 0, 0, &xclip, 1, Unsorted);
181 ltk_surface_fill_polygon(s, c, points, npoints);
182 XSetClipMask(s->window->renderdata->dpy, s->window->gc, None);
183 }
184
185 void
186 ltk_surface_fill_ellipse(ltk_surface *s, ltk_color *c, ltk_rect rect) {
187 XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
188 XFillArc(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h, 0, 360 * 64);
189 }
190
191 void
192 ltk_surface_fill_ellipse_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip) {
193 /* NOTE: XRectangle only uses short, so this could cause issues */
194 XRectangle xclip = {clip.x, clip.y, clip.w, clip.h};
195 XSetClipRectangles(s->window->renderdata->dpy, s->window->gc, 0, 0, &xclip, 1, Unsorted);
196 ltk_surface_fill_ellipse(s, c, rect);
197 XSetClipMask(s->window->renderdata->dpy, s->window->gc, None);
198 }
199
200 void
201 ltk_surface_copy(ltk_surface *src, ltk_surface *dst, ltk_rect src_rect, int dst_x, int dst_y) {
202 XCopyArea(
203 src->window->renderdata->dpy, src->d, dst->d, src->window->gc,
204 src_rect.x, src_rect.y, src_rect.w, src_rect.h, dst_x, dst_y
205 );
206 }
207
208 /* TODO */
209 /*
210 void
211 ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width) {
212 }
213
214 void
215 ltk_surface_fill_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2) {
216 }
217
218 void
219 ltk_surface_draw_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r, int line_width) {
220 }
221
222 void
223 ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r) {
224 }
225 */
226
227 /* FIXME: move this to a file where it makes more sense */
228 /* blatantly stolen from st */
229 static void ximinstantiate(Display *dpy, XPointer client, XPointer call);
230 static void ximdestroy(XIM xim, XPointer client, XPointer call);
231 static int xicdestroy(XIC xim, XPointer client, XPointer call);
232 static int ximopen(ltk_renderwindow *window);
233
234 static void
235 ximdestroy(XIM xim, XPointer client, XPointer call) {
236 (void)xim;
237 (void)call;
238 ltk_renderwindow *window = (ltk_renderwindow *)client;
239 window->xim = NULL;
240 XRegisterIMInstantiateCallback(
241 window->renderdata->dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
242 );
243 XFree(window->spotlist);
244 }
245
246 static int
247 xicdestroy(XIC xim, XPointer client, XPointer call) {
248 (void)xim;
249 (void)call;
250 ltk_renderwindow *window = (ltk_renderwindow *)client;
251 window->xic = NULL;
252 return 1;
253 }
254
255 static int
256 ximopen(ltk_renderwindow *window) {
257 XIMCallback imdestroy = { .client_data = (XPointer)window, .callback = ximdestroy };
258 XICCallback icdestroy = { .client_data = (XPointer)window, .callback = xicdestroy };
259
260 window->xim = XOpenIM(window->renderdata->dpy, NULL, NULL, NULL);
261 if (window->xim == NULL)
262 return 0;
263
264 if (XSetIMValues(window->xim, XNDestroyCallback, &imdestroy, NULL))
265 ltk_warn("XSetIMValues: Could not set XNDestroyCallback.\n");
266
267 window->spotlist = XVaCreateNestedList(0, XNSpotLocation, &window->spot, NULL);
268
269 if (window->xic == NULL) {
270 window->xic = XCreateIC(
271 window->xim, XNInputStyle,
272 XIMPreeditNothing | XIMStatusNothing,
273 XNClientWindow, window->xwindow,
274 XNDestroyCallback, &icdestroy, NULL
275 );
276 }
277 if (window->xic == NULL)
278 ltk_warn("XCreateIC: Could not create input context.\n");
279
280 return 1;
281 }
282
283 static void
284 ximinstantiate(Display *dpy, XPointer client, XPointer call) {
285 (void)call;
286 ltk_renderwindow *window = (ltk_renderwindow *)client;
287 if (ximopen(window)) {
288 XUnregisterIMInstantiateCallback(
289 dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
290 );
291 }
292 }
293
294 void
295 ltk_renderer_set_imspot(ltk_renderwindow *window, int x, int y) {
296 if (window->xic == NULL)
297 return;
298 window->spot.x = x;
299 window->spot.y = y;
300 XSetICValues(window->xic, XNPreeditAttributes, window->spotlist, NULL);
301 }
302
303 ltk_renderdata *
304 ltk_renderer_create(void) {
305 /* FIXME: this might not be the best place for this */
306 XSetLocaleModifiers("");
307 ltk_renderdata *renderdata = ltk_malloc(sizeof(ltk_renderdata));
308 renderdata->dpy = XOpenDisplay(NULL);
309 renderdata->screen = DefaultScreen(renderdata->dpy);
310 renderdata->db_enabled = 0;
311 /* based on http://wili.cc/blog/xdbe.html */
312 int major, minor;
313 if (XdbeQueryExtension(renderdata->dpy, &major, &minor)) {
314 int num_screens = 1;
315 Drawable screens[] = {DefaultRootWindow(renderdata->dpy)};
316 XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
317 renderdata->dpy, screens, &num_screens
318 );
319 if (!info || num_screens < 1 || info->count < 1) {
320 ltk_fatal("No visuals support Xdbe.");
321 }
322 XVisualInfo xvisinfo_templ;
323 /* we know there's at least one */
324 xvisinfo_templ.visualid = info->visinfo[0].visual;
325 /* FIXME: proper screen number? */
326 xvisinfo_templ.screen = 0;
327 xvisinfo_templ.depth = info->visinfo[0].depth;
328 int matches;
329 XVisualInfo *xvisinfo_match = XGetVisualInfo(
330 renderdata->dpy,
331 VisualIDMask | VisualScreenMask | VisualDepthMask,
332 &xvisinfo_templ, &matches
333 );
334 if (!xvisinfo_match || matches < 1) {
335 ltk_fatal("Couldn't match a Visual with double buffering.\n");
336 }
337 renderdata->vis = xvisinfo_match->visual;
338 /* FIXME: is it legal to free this while keeping the visual? */
339 XFree(xvisinfo_match);
340 XdbeFreeVisualInfo(info);
341 renderdata->db_enabled = 1;
342 } else {
343 renderdata->vis = DefaultVisual(renderdata->dpy, renderdata->screen);
344 ltk_warn("No Xdbe support.\n");
345 }
346 renderdata->cm = DefaultColormap(renderdata->dpy, renderdata->screen);
347 renderdata->wm_delete_msg = XInternAtom(renderdata->dpy, "WM_DELETE_WINDOW", False);
348 renderdata->depth = DefaultDepth(renderdata->dpy, renderdata->screen);
349 renderdata->xkb_supported = 1;
350 renderdata->xkb_event_type = 0;
351 /* FIXME: check version */
352 if (!XkbQueryExtension(renderdata->dpy, 0, &renderdata->xkb_event_type, NULL, &major, &minor)) {
353 ltk_warn("XKB not supported.\n");
354 renderdata->xkb_supported = 0;
355 } else {
356 /* This should select the events when the keyboard mapping changes.
357 * When e.g. 'setxkbmap us' is executed, two events are sent, but I
358 * haven't figured out how to change that. When the xkb layout
359 * switching is used (e.g. 'setxkbmap -option grp:shifts_toggle'),
360 * this issue does not occur because only a state event is sent. */
361 XkbSelectEvents(
362 renderdata->dpy, XkbUseCoreKbd,
363 XkbNewKeyboardNotifyMask, XkbNewKeyboardNotifyMask
364 );
365 XkbSelectEventDetails(
366 renderdata->dpy, XkbUseCoreKbd, XkbStateNotify,
367 XkbAllStateComponentsMask, XkbGroupStateMask
368 );
369 }
370 renderdata->monitors = NULL;
371 renderdata->xrandr_supported = 0;
372 renderdata->root_window = XRootWindow(renderdata->dpy, renderdata->screen);
373 #if USE_XRANDR
374 if (!XRRQueryVersion(renderdata->dpy, &major, &minor)) {
375 ltk_warn("XRandR not supported.\n");
376 } else if (major < 1 || (major == 1 && minor < 5)) {
377 ltk_warn("X server only supports XRandR version %d.%d. Need at least 1.5.\n", major, minor);
378 } else {
379 /* FIXME: check if XRRQueryExtension allows passing NULL */
380 int tmp;
381 if (!XRRQueryExtension(renderdata->dpy, &renderdata->xrandr_event_type, &tmp)) {
382 ltk_warn("Unable to get XRandR event type.\n");
383 } else {
384 renderdata->xrandr_supported = 1;
385 /* FIXME: is this mask correct? */
386 XRRSelectInput(renderdata->dpy, renderdata->root_window, RRScreenChangeNotifyMask);
387 }
388 }
389
390 #endif
391
392 return renderdata;
393 }
394
395 ltk_renderwindow *
396 ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h, unsigned int initial_dpi) {
397 XSetWindowAttributes attrs;
398 ltk_renderwindow *window = ltk_malloc(sizeof(ltk_renderwindow));
399 window->renderdata = data;
400 memset(&attrs, 0, sizeof(attrs));
401 attrs.background_pixel = BlackPixel(data->dpy, data->screen);
402 attrs.colormap = data->cm;
403 attrs.border_pixel = WhitePixel(data->dpy, data->screen);
404 /* this causes the window contents to be kept
405 * when it is resized, leading to less flicker */
406 attrs.bit_gravity = NorthWestGravity;
407 attrs.event_mask =
408 ExposureMask | KeyPressMask | KeyReleaseMask |
409 ButtonPressMask | ButtonReleaseMask |
410 StructureNotifyMask | PointerMotionMask;
411 /* FIXME: conversion between signed and unsigned */
412 window->rect = (ltk_rect){x, y, w, h};
413 /* FIXME: set border width */
414 window->xwindow = XCreateWindow(
415 data->dpy, DefaultRootWindow(data->dpy), x, y,
416 w, h, 0, data->depth,
417 InputOutput, data->vis,
418 CWBackPixel | CWColormap | CWBitGravity | CWEventMask | CWBorderPixel, &attrs
419 );
420
421 if (data->db_enabled) {
422 window->back_buf = XdbeAllocateBackBufferName(
423 data->dpy, window->xwindow, XdbeBackground
424 );
425 } else {
426 window->back_buf = window->xwindow;
427 }
428 window->drawable = window->back_buf;
429 window->gc = XCreateGC(data->dpy, window->xwindow, 0, 0);
430 XSetStandardProperties(
431 data->dpy, window->xwindow,
432 title, NULL, None, NULL, 0, NULL
433 );
434 /* FIXME: check return value */
435 XSetWMProtocols(data->dpy, window->xwindow, &data->wm_delete_msg, 1);
436
437 window->xic = NULL;
438 window->xim = NULL;
439 if (!ximopen(window)) {
440 XRegisterIMInstantiateCallback(
441 window->renderdata->dpy, NULL, NULL, NULL,
442 ximinstantiate, (XPointer)window
443 );
444 }
445
446 XClearWindow(window->renderdata->dpy, window->xwindow);
447 XMapRaised(window->renderdata->dpy, window->xwindow);
448
449 window->dpi = initial_dpi;
450 ltk_recalc_renderwindow_dpi(window);
451
452 return window;
453 }
454
455 unsigned int
456 ltk_renderer_get_window_dpi(ltk_renderwindow *window) {
457 return window->dpi;
458 }
459
460 void
461 ltk_renderer_destroy_window(ltk_renderwindow *window) {
462 XFreeGC(window->renderdata->dpy, window->gc);
463 if (window->spotlist)
464 XFree(window->spotlist);
465 /* FIXME: destroy xim/xic? */
466 XDestroyWindow(window->renderdata->dpy, window->xwindow);
467 ltk_free(window);
468 }
469
470 void
471 ltk_renderer_destroy(ltk_renderdata *renderdata) {
472 if (renderdata->monitors)
473 ltk_array_destroy(moninfo, renderdata->monitors);
474 XCloseDisplay(renderdata->dpy);
475 /* FIXME: destroy visual, wm_delete_msg, etc.? */
476 ltk_free(renderdata);
477 }
478
479 /* FIXME: this is a completely random collection of properties and should be
480 changed to a more sensible list */
481 void
482 ltk_renderer_set_window_properties(ltk_renderwindow *window, ltk_color *bg) {
483 XSetWindowBackground(window->renderdata->dpy, window->xwindow, bg->xcolor.pixel);
484 }
485
486 void
487 ltk_renderer_swap_buffers(ltk_renderwindow *window) {
488 XdbeSwapInfo swap_info;
489 swap_info.swap_window = window->xwindow;
490 swap_info.swap_action = XdbeBackground;
491 if (!XdbeSwapBuffers(window->renderdata->dpy, &swap_info, 1))
492 ltk_fatal("Unable to swap buffers.\n");
493 XFlush(window->renderdata->dpy);
494 }
495
496 unsigned long
497 ltk_renderer_get_window_id(ltk_renderwindow *window) {
498 return (unsigned long)window->xwindow;
499 }