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