Graphviz 13.0.0~dev.20241220.2304
Loading...
Searching...
No Matches
gvdevice_xlib.c
Go to the documentation of this file.
1/*************************************************************************
2 * Copyright (c) 2011 AT&T Intellectual Property
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License v1.0
5 * which accompanies this distribution, and is available at
6 * https://www.eclipse.org/legal/epl-v10.html
7 *
8 * Contributors: Details at https://graphviz.org
9 *************************************************************************/
10
11#include "config.h"
12
13#include <assert.h>
14#include <fcntl.h>
15#include <inttypes.h>
16#include <limits.h>
17#include <math.h>
18#include <stdbool.h>
19#include <stdio.h>
20#include <stdlib.h>
21#include <string.h>
22#include <unistd.h>
23#ifdef HAVE_SYS_TIME_H
24#include <sys/time.h>
25#endif
26#ifdef HAVE_SYS_IOCTL_H
27#include <sys/ioctl.h>
28#endif
29#include <sys/types.h>
30#ifdef HAVE_SYS_SELECT_H
31#include <sys/select.h>
32#endif
33#ifdef HAVE_SYS_INOTIFY_H
34#include <sys/inotify.h>
35#ifdef __sun
36#include <sys/filio.h>
37#endif
38#endif
39#include <errno.h>
40
41#include <gvc/gvplugin_device.h>
42#include <util/agxbuf.h>
43#include <util/exit.h>
44#include <util/gv_math.h>
45#include <util/prisize_t.h>
46
47#include <cairo.h>
48#ifdef CAIRO_HAS_XLIB_SURFACE
49#include <X11/Xutil.h>
50#include <X11/extensions/Xrender.h>
51#include <cairo-xlib.h>
52
53typedef struct window_xlib_s {
54 Window win;
55 uint64_t event_mask;
56 Pixmap pix;
57 GC gc;
58 Visual *visual;
59 Colormap cmap;
60 int depth;
61 Atom wm_delete_window_atom;
62} window_t;
63
64static void handle_configure_notify(GVJ_t *job, XConfigureEvent *cev) {
65 /*FIXME - should allow for margins */
66 /* - similar zoom_to_fit code exists in: */
67 /* plugin/gtk/callbacks.c */
68 /* plugin/xlib/gvdevice_xlib.c */
69 /* lib/gvc/gvevent.c */
70
71 assert(cev->width >= 0 && "Xlib returned an event with negative width");
72 assert(cev->height >= 0 && "Xlib returned an event with negative height");
73
74 job->zoom *= 1 + fmin(((double)cev->width - job->width) / job->width,
75 ((double)cev->height - job->height) / job->height);
76 if ((unsigned)cev->width > job->width || (unsigned)cev->height > job->height)
77 job->has_grown = true;
78 job->width = (unsigned)cev->width;
79 job->height = (unsigned)cev->height;
80 job->needs_refresh = true;
81}
82
83static void handle_expose(GVJ_t *job, XExposeEvent *eev) {
84 window_t *window;
85
86 window = job->window;
87 assert(eev->width >= 0 &&
88 "Xlib returned an expose event with negative width");
89 assert(eev->height >= 0 &&
90 "Xlib returned an expose event with negative height");
91 XCopyArea(eev->display, window->pix, eev->window, window->gc, eev->x, eev->y,
92 (unsigned)eev->width, (unsigned)eev->height, eev->x, eev->y);
93}
94
95static void handle_client_message(GVJ_t *job, XClientMessageEvent *cmev) {
96 window_t *window;
97
98 window = job->window;
99 if (cmev->format == 32 &&
100 (Atom)cmev->data.l[0] == window->wm_delete_window_atom)
101 graphviz_exit(0);
102}
103
104static bool handle_keypress(GVJ_t *job, XKeyEvent *kev) {
105 const KeyCode *const keycodes = job->keycodes;
106 for (size_t i = 0; i < job->numkeys; i++) {
107 if (kev->keycode == keycodes[i])
108 return job->keybindings[i].callback(job) != 0;
109 }
110 return false;
111}
112
113static Visual *find_argb_visual(Display *dpy, int scr) {
114 XVisualInfo *xvi;
115 XVisualInfo template;
116 int nvi;
117 int i;
118 XRenderPictFormat *format;
119 Visual *visual;
120
121 template.screen = scr;
122 template.depth = 32;
123 template.class = TrueColor;
124 xvi =
125 XGetVisualInfo(dpy, VisualScreenMask | VisualDepthMask | VisualClassMask,
126 &template, &nvi);
127 if (!xvi)
128 return 0;
129 visual = 0;
130 for (i = 0; i < nvi; i++) {
131 format = XRenderFindVisualFormat(dpy, xvi[i].visual);
132 if (format->type == PictTypeDirect && format->direct.alphaMask) {
133 visual = xvi[i].visual;
134 break;
135 }
136 }
137
138 XFree(xvi);
139 return visual;
140}
141
142static void browser_show(GVJ_t *job) {
143 char *exec_argv[3] = {BROWSER, NULL, NULL};
144 pid_t pid;
145
146 exec_argv[1] = job->selected_href;
147
148 pid = fork();
149 if (pid == -1) {
150 fprintf(stderr, "fork failed: %s\n", strerror(errno));
151 } else if (pid == 0) {
152 execvp(exec_argv[0], exec_argv);
153 fprintf(stderr, "error starting %s: %s\n", exec_argv[0], strerror(errno));
154 }
155}
156
157static int handle_xlib_events(GVJ_t *firstjob, Display *dpy) {
158 GVJ_t *job;
159 window_t *window;
160 XEvent xev;
161 pointf pointer;
162 int rc = 0;
163
164 while (XPending(dpy)) {
165 XNextEvent(dpy, &xev);
166
167 for (job = firstjob; job; job = job->next_active) {
168 window = job->window;
169 if (xev.xany.window == window->win) {
170 switch (xev.xany.type) {
171 case ButtonPress:
172 pointer.x = xev.xbutton.x;
173 pointer.y = xev.xbutton.y;
174 assert(xev.xbutton.button <= (unsigned)INT_MAX &&
175 "Xlib returned invalid button event");
176 job->callbacks->button_press(job, (int)xev.xbutton.button, pointer);
177 rc++;
178 break;
179 case MotionNotify:
180 if (job->button) { /* only interested while a button is pressed */
181 pointer.x = xev.xbutton.x;
182 pointer.y = xev.xbutton.y;
183 job->callbacks->motion(job, pointer);
184 rc++;
185 }
186 break;
187 case ButtonRelease:
188 pointer.x = xev.xbutton.x;
189 pointer.y = xev.xbutton.y;
190 assert(xev.xbutton.button <= (unsigned)INT_MAX &&
191 "Xlib returned invalid button event");
192 job->callbacks->button_release(job, (int)xev.xbutton.button, pointer);
193 if (job->selected_href && job->selected_href[0] &&
194 xev.xbutton.button == 1)
195 browser_show(job);
196 rc++;
197 break;
198 case KeyPress:
199 if (handle_keypress(job, &xev.xkey))
200 return -1; /* exit code */
201 rc++;
202 break;
203 case ConfigureNotify:
204 handle_configure_notify(job, &xev.xconfigure);
205 rc++;
206 break;
207 case Expose:
208 handle_expose(job, &xev.xexpose);
209 rc++;
210 break;
211 case ClientMessage:
212 handle_client_message(job, &xev.xclient);
213 rc++;
214 break;
215 default:
216 break;
217 }
218 break;
219 }
220 }
221 }
222 return rc;
223}
224
225static void update_display(GVJ_t *job, Display *dpy) {
226 window_t *window;
227 cairo_surface_t *surface;
228
229 window = job->window;
230
231 // window geometry is set to fixed values
232 assert(job->width <= (unsigned)INT_MAX && "out of range width");
233 assert(job->height <= (unsigned)INT_MAX && "out of range height");
234
235 if (job->has_grown) {
236 XFreePixmap(dpy, window->pix);
237 assert(window->depth >= 0 && "Xlib returned invalid window depth");
238 window->pix = XCreatePixmap(dpy, window->win, job->width, job->height,
239 (unsigned)window->depth);
240 job->has_grown = false;
241 job->needs_refresh = true;
242 }
243 if (job->needs_refresh) {
244 XFillRectangle(dpy, window->pix, window->gc, 0, 0, job->width, job->height);
245 surface = cairo_xlib_surface_create(dpy, window->pix, window->visual,
246 (int)job->width, (int)job->height);
247 job->context = cairo_create(surface);
248 job->external_context = true;
249 job->callbacks->refresh(job);
250 cairo_surface_destroy(surface);
251 XCopyArea(dpy, window->pix, window->win, window->gc, 0, 0, job->width,
252 job->height, 0, 0);
253 job->needs_refresh = false;
254 }
255}
256
257static void init_window(GVJ_t *job, Display *dpy, int scr) {
258 int argb = 0;
259 const char *base = "";
260 XGCValues gcv;
261 XSetWindowAttributes attributes;
262 XWMHints *wmhints;
263 XSizeHints *normalhints;
264 XClassHint *classhint;
265 uint64_t attributemask = 0;
266 window_t *window;
267 double zoom_to_fit;
268
269 window = malloc(sizeof(window_t));
270 if (window == NULL) {
271 fprintf(stderr, "Failed to malloc window_t\n");
272 return;
273 }
274
275 unsigned w =
276 480; /* FIXME - w,h should be set by a --geometry commandline option */
277 unsigned h = 325;
278
279 zoom_to_fit = fmin((double)w / job->width, (double)h / job->height);
280 if (zoom_to_fit < 1.0) /* don't make bigger */
281 job->zoom *= zoom_to_fit;
282
283 job->width = w; /* use window geometry */
284 job->height = h;
285
286 job->window = window;
287 job->fit_mode = false;
288 job->needs_refresh = true;
289
290 if (argb && (window->visual = find_argb_visual(dpy, scr))) {
291 window->cmap =
292 XCreateColormap(dpy, RootWindow(dpy, scr), window->visual, AllocNone);
293 attributes.override_redirect = False;
294 attributes.background_pixel = 0;
295 attributes.border_pixel = 0;
296 attributes.colormap = window->cmap;
297 attributemask =
298 (CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWColormap);
299 window->depth = 32;
300 } else {
301 window->cmap = DefaultColormap(dpy, scr);
302 window->visual = DefaultVisual(dpy, scr);
303 attributes.background_pixel = WhitePixel(dpy, scr);
304 attributes.border_pixel = BlackPixel(dpy, scr);
305 attributemask = (CWBackPixel | CWBorderPixel);
306 window->depth = DefaultDepth(dpy, scr);
307 }
308
309 window->win = XCreateWindow(dpy, RootWindow(dpy, scr), 0, 0, job->width,
310 job->height, 0, window->depth, InputOutput,
311 window->visual, attributemask, &attributes);
312
313 agxbuf name = {0};
314 agxbprint(&name, "graphviz: %s", base);
315
316 normalhints = XAllocSizeHints();
317 normalhints->flags = 0;
318 normalhints->x = 0;
319 normalhints->y = 0;
320 assert(job->width <= (unsigned)INT_MAX && "out of range width");
321 normalhints->width = (int)job->width;
322 assert(job->height <= (unsigned)INT_MAX && "out of range height");
323 normalhints->height = (int)job->height;
324
325 classhint = XAllocClassHint();
326 classhint->res_name = "graphviz";
327 classhint->res_class = "Graphviz";
328
329 wmhints = XAllocWMHints();
330 wmhints->flags = InputHint;
331 wmhints->input = True;
332
333 Xutf8SetWMProperties(dpy, window->win, agxbuse(&name), base, 0, 0,
334 normalhints, wmhints, classhint);
335 XFree(wmhints);
336 XFree(classhint);
337 XFree(normalhints);
338 agxbfree(&name);
339
340 assert(window->depth >= 0 && "Xlib returned invalid window depth");
341 window->pix = XCreatePixmap(dpy, window->win, job->width, job->height,
342 (unsigned)window->depth);
343 if (argb)
344 gcv.foreground = 0;
345 else
346 gcv.foreground = WhitePixel(dpy, scr);
347 window->gc = XCreateGC(dpy, window->pix, GCForeground, &gcv);
348 update_display(job, dpy);
349
350 window->event_mask =
351 (ButtonPressMask | ButtonReleaseMask | PointerMotionMask | KeyPressMask |
352 StructureNotifyMask | ExposureMask);
353 XSelectInput(dpy, window->win, (long)window->event_mask);
354 window->wm_delete_window_atom = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
355 XSetWMProtocols(dpy, window->win, &window->wm_delete_window_atom, 1);
356 XMapWindow(dpy, window->win);
357}
358
359static int handle_stdin_events(GVJ_t *job) {
360
361 if (feof(stdin))
362 return -1;
363 job->callbacks->read(job, job->input_filename, job->layout_type);
364
365 return 1;
366}
367
368#ifdef HAVE_SYS_INOTIFY_H
369static int handle_file_events(GVJ_t *job, int inotify_fd) {
370 int avail, ret, len, rc = 0;
371 char *bf, *p;
372 struct inotify_event *event;
373
374 ret = ioctl(inotify_fd, FIONREAD, &avail);
375 if (ret < 0) {
376 fprintf(stderr, "ioctl() failed\n");
377 return -1;
378 }
379
380 if (avail) {
381 assert(avail > 0 && "invalid value from FIONREAD");
382 void *buf = malloc((size_t)avail);
383 if (!buf) {
384 fprintf(stderr, "out of memory (could not allocate %d bytes)\n", avail);
385 return -1;
386 }
387 len = (int)read(inotify_fd, buf, (size_t)avail);
388 if (len != avail) {
389 fprintf(stderr, "avail = %d, len = %d\n", avail, len);
390 free(buf);
391 return -1;
392 }
393 bf = buf;
394 while (len > 0) {
395 event = (struct inotify_event *)bf;
396 if (event->mask == IN_MODIFY) {
397 p = strrchr(job->input_filename, '/');
398 if (p)
399 p++;
400 else
401 p = job->input_filename;
402 if (strcmp(event->name, p) == 0) {
403 job->callbacks->read(job, job->input_filename, job->layout_type);
404 rc++;
405 }
406 }
407 size_t ln = event->len + sizeof(struct inotify_event);
408 assert(ln <= (size_t)len);
409 bf += ln;
410 len -= (int)ln;
411 }
412 free(buf);
413 if (len != 0) {
414 fprintf(stderr, "length miscalculation, len = %d\n", len);
415 return -1;
416 }
417 }
418 return rc;
419}
420#endif
421
422static void xlib_initialize(GVJ_t *firstjob) {
423 KeySym keysym;
424 KeyCode *keycodes;
425 const char *display_name = NULL;
426 int scr;
427
428 Display *dpy = XOpenDisplay(display_name);
429 if (dpy == NULL) {
430 fprintf(stderr, "Failed to open XLIB display: %s\n", XDisplayName(NULL));
431 return;
432 }
433 scr = DefaultScreen(dpy);
434
435 firstjob->screen = scr;
436
437 keycodes = malloc(firstjob->numkeys * sizeof(KeyCode));
438 if (firstjob->numkeys > 0 && keycodes == NULL) {
439 fprintf(stderr, "Failed to malloc %" PRISIZE_T "*KeyCode\n",
440 firstjob->numkeys);
441 XCloseDisplay(dpy);
442 return;
443 }
444 for (size_t i = 0; i < firstjob->numkeys; i++) {
445 keysym = XStringToKeysym(firstjob->keybindings[i].keystring);
446 if (keysym == NoSymbol)
447 fprintf(stderr, "ERROR: No keysym for \"%s\"\n",
448 firstjob->keybindings[i].keystring);
449 else
450 keycodes[i] = XKeysymToKeycode(dpy, keysym);
451 }
452 firstjob->keycodes = keycodes;
453
454 firstjob->device_dpi.x =
455 DisplayWidth(dpy, scr) * 25.4 / DisplayWidthMM(dpy, scr);
456 firstjob->device_dpi.y =
457 DisplayHeight(dpy, scr) * 25.4 / DisplayHeightMM(dpy, scr);
458 firstjob->device_sets_dpi = true;
459
460 firstjob->display = dpy;
461}
462
463static void xlib_finalize(GVJ_t *firstjob) {
464 GVJ_t *job;
465 Display *dpy = firstjob->display;
466 int scr = firstjob->screen;
467 KeyCode *keycodes = firstjob->keycodes;
468 int numfds, stdin_fd = 0, xlib_fd, ret, events;
469 fd_set rfds;
470 bool watching_stdin_p = false;
471#ifdef HAVE_SYS_INOTIFY_H
472 int wd = 0;
473 int inotify_fd = 0;
474 bool watching_file_p = false;
475 char *p, *cwd = NULL;
476
477#ifdef HAVE_INOTIFY_INIT1
478#ifdef IN_CLOSEXEC
479 inotify_fd = inotify_init1(IN_CLOSEXEC);
480#else
481 inotify_fd = inotify_init1(IN_CLOEXEC);
482#endif
483#else
484 inotify_fd = inotify_init();
485 if (inotify_fd >= 0) {
486 const int flags = fcntl(inotify_fd, F_GETFD);
487 if (fcntl(inotify_fd, F_SETFD, flags | FD_CLOEXEC) < 0) {
488 fprintf(stderr, "setting FD_CLOEXEC failed\n");
489 return;
490 }
491 }
492#endif
493 if (inotify_fd < 0) {
494 fprintf(stderr, "inotify_init() failed\n");
495 return;
496 }
497#endif
498
499 /* skip if initialization previously failed */
500 if (dpy == NULL) {
501 return;
502 }
503
504 numfds = xlib_fd = XConnectionNumber(dpy);
505
506 if (firstjob->input_filename) {
507 if (firstjob->graph_index == 0) {
508#ifdef HAVE_SYS_INOTIFY_H
509 watching_file_p = true;
510
511 agxbuf dir = {0};
512 if (firstjob->input_filename[0] != '/') {
513 cwd = getcwd(NULL, 0);
514 agxbprint(&dir, "%s/%s", cwd, firstjob->input_filename);
515 free(cwd);
516 } else {
517 agxbput(&dir, firstjob->input_filename);
518 }
519 char *dirstr = agxbuse(&dir);
520 p = strrchr(dirstr, '/');
521 *p = '\0';
522
523 wd = inotify_add_watch(inotify_fd, dirstr, IN_MODIFY);
524 agxbfree(&dir);
525
526 numfds = imax(inotify_fd, numfds);
527#endif
528 }
529 } else {
530 watching_stdin_p = true;
531#ifdef F_DUPFD_CLOEXEC
532 stdin_fd = fcntl(STDIN_FILENO, F_DUPFD_CLOEXEC, 0);
533#else
534 stdin_fd = fcntl(STDIN_FILENO, F_DUPFD, 0);
535 (void)fcntl(stdin_fd, F_SETFD, fcntl(stdin_fd, F_GETFD) | FD_CLOEXEC);
536#endif
537 numfds = imax(stdin_fd, numfds);
538 }
539
540 for (job = firstjob; job; job = job->next_active)
541 init_window(job, dpy, scr);
542
543 /* This is the event loop */
544 FD_ZERO(&rfds);
545 while (1) {
546 events = 0;
547
548#ifdef HAVE_SYS_INOTIFY_H
549 if (watching_file_p) {
550 if (FD_ISSET(inotify_fd, &rfds)) {
551 ret = handle_file_events(firstjob, inotify_fd);
552 if (ret < 0)
553 break;
554 events += ret;
555 }
556 FD_SET(inotify_fd, &rfds);
557 }
558#endif
559
560 if (watching_stdin_p) {
561 if (FD_ISSET(stdin_fd, &rfds)) {
562 ret = handle_stdin_events(firstjob);
563 if (ret < 0) {
564 watching_stdin_p = false;
565 FD_CLR(stdin_fd, &rfds);
566 }
567 events += ret;
568 }
569 if (watching_stdin_p)
570 FD_SET(stdin_fd, &rfds);
571 }
572
573 ret = handle_xlib_events(firstjob, dpy);
574 if (ret < 0)
575 break;
576 events += ret;
577 FD_SET(xlib_fd, &rfds);
578
579 if (events) {
580 for (job = firstjob; job; job = job->next_active)
581 update_display(job, dpy);
582 XFlush(dpy);
583 }
584
585 ret = select(numfds + 1, &rfds, NULL, NULL, NULL);
586 if (ret < 0) {
587 fprintf(stderr, "select() failed\n");
588 break;
589 }
590 }
591
592#ifdef HAVE_SYS_INOTIFY_H
593 if (watching_file_p)
594 ret = inotify_rm_watch(inotify_fd, wd);
595#endif
596
597 XCloseDisplay(dpy);
598 firstjob->display = NULL;
599 free(keycodes);
600 firstjob->keycodes = NULL;
601}
602
603static gvdevice_features_t device_features_xlib = {
605 {0., 0.}, /* default margin - points */
606 {0., 0.}, /* default page width, height - points */
607 {96., 96.}, /* dpi */
608};
609
610static gvdevice_engine_t device_engine_xlib = {
611 xlib_initialize,
612 NULL, /* xlib_format */
613 xlib_finalize,
614};
615#endif
616
618#ifdef CAIRO_HAS_XLIB_SURFACE
619 {0, "xlib:cairo", 0, &device_engine_xlib, &device_features_xlib},
620 {0, "x11:cairo", 0, &device_engine_xlib, &device_features_xlib},
621#endif
622 {0, NULL, 0, NULL, NULL}};
static void agxbfree(agxbuf *xb)
free any malloced resources
Definition agxbuf.h:78
static int agxbprint(agxbuf *xb, const char *fmt,...)
Printf-style output to an agxbuf.
Definition agxbuf.h:234
static WUR char * agxbuse(agxbuf *xb)
Definition agxbuf.h:307
static NORETURN void graphviz_exit(int status)
Definition exit.h:23
static int flags
Definition gc.c:61
static double len(glCompPoint p)
Definition glutils.c:150
void * malloc(YYSIZE_T)
void free(void *)
node NULL
Definition grammar.y:163
Agraph_t * read(FILE *f)
Definition gv.cpp:60
Arithmetic helper functions.
static int imax(int a, int b)
maximum of two integers
Definition gv_math.h:24
#define GVDEVICE_DOES_TRUECOLOR
Definition gvcjob.h:90
#define GVDEVICE_EVENTS
Definition gvcjob.h:89
agxbput(xb, staging)
gvplugin_installed_t gvdevice_types_xlib[]
GVIO_API const char * format
Definition gvio.h:51
#define PRISIZE_T
PRIu64 alike for printing size_t
Definition prisize_t.h:27
int screen
Definition gvcjob.h:293
bool fit_mode
Definition gvcjob.h:336
unsigned char button
Definition gvcjob.h:342
gvdevice_callbacks_t * callbacks
Definition gvcjob.h:288
char * selected_href
Definition gvcjob.h:351
bool needs_refresh
Definition gvcjob.h:337
gvevent_key_binding_t * keybindings
Definition gvcjob.h:356
void * context
Definition gvcjob.h:295
void * keycodes
Definition gvcjob.h:358
bool external_context
Definition gvcjob.h:296
void * display
Definition gvcjob.h:292
pointf device_dpi
Definition gvcjob.h:289
double zoom
Definition gvcjob.h:318
const char * layout_type
Definition gvcjob.h:274
unsigned int width
Definition gvcjob.h:327
void * window
Definition gvcjob.h:353
bool has_grown
Definition gvcjob.h:339
bool device_sets_dpi
Definition gvcjob.h:290
char * input_filename
Definition gvcjob.h:271
int graph_index
Definition gvcjob.h:272
size_t numkeys
Definition gvcjob.h:357
GVJ_t * next_active
Definition gvcjob.h:265
unsigned int height
Definition gvcjob.h:328
void(* button_release)(GVJ_t *job, int button, pointf pointer)
Definition gvcjob.h:150
void(* refresh)(GVJ_t *job)
Definition gvcjob.h:148
void(* button_press)(GVJ_t *job, int button, pointf pointer)
Definition gvcjob.h:149
void(* motion)(GVJ_t *job, pointf pointer)
Definition gvcjob.h:151
void(* read)(GVJ_t *job, const char *filename, const char *layout)
Definition gvcjob.h:154
gvevent_key_callback_t callback
Definition gvcjob.h:163
ingroup plugin_api
Definition gvplugin.h:35
double x
Definition geom.h:29
double y
Definition geom.h:29