2
votes

I'm trying to use the Cairo graphics library on Linux in C to make a pretty lightweight x11 GUI.

After trying very hard to follow the woefully incomplete guide that cairo gives for x11, this is the best I've got:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <cairo.h>
#include <cairo-xlib.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xrender.h>
#include <X11/extensions/renderproto.h>

//This function should give us a new x11 surface to draw on.
cairo_surface_t* create_x11_surface(int x, int y)
{
    Display* d;
    Drawable da;
    int screen;
    cairo_surface_t* sfc;

    if((d = XOpenDisplay(NULL)) == NULL)
    {
        printf("failed to open display\n");
        exit(1);
    }

    screen = DefaultScreen(d);
    da = XCreateSimpleWindow(d, DefaultRootWindow(d), 0, 0, x, y, 0, 0, 0);
    XSelectInput(d, da, ButtonPressMask | KeyPressMask);
    XMapWindow(d, da);

    sfc = cairo_xlib_surface_create(d, da, DefaultVisual(d, screen), x, y);
    cairo_xlib_surface_set_size(sfc, x, y);

    return sfc;
}

int main(int argc, char** argv)
{
    //create a new cairo surface in an x11 window as well as a cairo_t* to draw
    //on the x11 window with.
    cairo_surface_t* surface = create_x11_surface(300, 200);
    cairo_t* cr = cairo_create(surface);

    while(1)
    {
        //save the empty drawing for the next time through the loop.
        cairo_push_group(cr);

        //draw some text
        cairo_select_font_face(cr, "serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
        cairo_set_font_size(cr, 32.0);
        cairo_set_source_rgb(cr, 0, 0, 1.0);
        cairo_move_to(cr, 10.0, 25.0);

        if((argc == 2) && (strnlen(argv[1], 100) < 50))
            cairo_show_text(cr, argv[1]);
        else
            cairo_show_text(cr, "usage: ./p1 <string>");

        //put the drawn text onto the screen(?)
        cairo_pop_group_to_source(cr);
        cairo_paint(cr);
        cairo_surface_flush(surface);

        //pause for a little bit.
        int c = getchar();

        //change the text around so we can see the screen update.
        for(int i = 0; i < strnlen(argv[1], 100); i++)
        {
            argv[1][i] = argv[1][i + 1];
        }

        if(c == 'q')
        {
            break;
        }
    }

    cairo_surface_destroy(surface);
    return 0;
}

On Linux systems that have Cairo installed, it can be compiled with

gcc -o myprog $(pkg-config --cflags --libs cairo x11) -std=gnu99 main.c

And it should be run with a single argument.

For reasons I don't understand at all, inserting the line

cairo_pop_group_to_source(cr);
cairo_paint(cr);
cairo_surface_write_to_png (surface, "hello.png");    //<--------- inserted
cairo_surface_flush(surface);

Puts something on the screen, but there are 2 problems:

  1. Text that I draw with this method is persistent, creating a smearing effect.
  2. I don't want some .png file mediating between my program and an x11 window. Data should be sent directly!
2
Your problem is in your choice of backend for cairo. You can choose png, pdf, svg, or you can write directly to a gtk window (which it appears you want to do). See the examples in Cairo Tutorial - zetcode for examples of each.David C. Rankin
No worries, just slow down a little bit and work through the tutorials and you will slowly begin to digest the different capabilities. No matter what you are trying to learn in C, remember it is not a race, and the details that you fly by when in a hurry -- really are important. So enjoy the ride. (not race).David C. Rankin
@DavidC.Rankin I don't know, in principle you can draw to a GdkPixmap which is a drawable just like a GdkWindow is, and then use the GdkPixmap as a X11 pixmap to draw to a X11 window. And when I've compiled cairo from source I recall it has a x11 backend but I am not sure.Iharob Al Asimi
That is a good point, I didn't climb down to the bottom of the cairo API and compare all the various backends. There may be some limitation on a direct write to X11, but the pixmap or window provided the direct write to screen and there are a number of direct X11 conversions at the bottom of that rabbit-hole to check.David C. Rankin
@johnny_boy If you're in a hurry, why are you not targetting something higher-level and more complete? Like, of course, GTK+?unwind

2 Answers

2
votes

Several issues:

  • In X11, the X11 server doesn't save what you drew to a window, but instead sends an ExposeEvent to your window that tells it to redraw. This means you get a black window, because you do not handle this event.
  • getchar only gives you something after a line break, so just typing something won't help.
  • libX11 buffers stuff and only sends it to the X11 server when you wait for an event (or the buffer fills up). Since you never wait for an event, it never flushes. Calling XFlush explicitly helps.
  • The group that you push is useless. Just get rid of it.
  • Your code to move the string one direction to the left easily goes beyond the end of the string. You apparently know this already, because you 'fixed' this with a strnlen.

Here is a little better solution, but it still gives you an initially black window, because you draw to it before it is mapped:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cairo-xlib.h>
#include <X11/Xlib.h>

//This function should give us a new x11 surface to draw on.
cairo_surface_t* create_x11_surface(Display *d, int x, int y)
{
    Drawable da;
    int screen;
    cairo_surface_t* sfc;

    screen = DefaultScreen(d);
    da = XCreateSimpleWindow(d, DefaultRootWindow(d), 0, 0, x, y, 0, 0, 0);
    XSelectInput(d, da, ButtonPressMask | KeyPressMask);
    XMapWindow(d, da);

    sfc = cairo_xlib_surface_create(d, da, DefaultVisual(d, screen), x, y);

    return sfc;
}

int main(int argc, char** argv)
{
    Display *d = XOpenDisplay(NULL);
    if (d == NULL) {
        fprintf(stderr, "Failed to open display\n");
        return 1;
    }
    //create a new cairo surface in an x11 window as well as a cairo_t* to draw
    //on the x11 window with.
    cairo_surface_t* surface = create_x11_surface(d, 300, 200);
    cairo_t* cr = cairo_create(surface);
    char *text = argv[1];
    size_t text_len = 0;

    if (argc != 2)
        text = NULL;
    else
        text_len = strlen(text);

    while(1)
    {
        // Clear the background
        cairo_set_source_rgb(cr, 0, 0, 0);
        cairo_paint(cr);

        //draw some text
        cairo_select_font_face(cr, "serif", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_BOLD);
        cairo_set_font_size(cr, 32.0);
        cairo_set_source_rgb(cr, 0, 0, 1.0);
        cairo_move_to(cr, 10.0, 25.0);

        if (text)
            cairo_show_text(cr, text);
        else
            cairo_show_text(cr, "usage: ./p1 <string>");

        cairo_surface_flush(surface);
        XFlush(d);

        //pause for a little bit.
        int c = getchar();

        //change the text around so we can see the screen update.
        memmove(text, &text[1], text_len);
        if (text_len > 0)
            text_len--;

        printf("got char %c\n", c);
        if(c == 'q')
        {
            break;
        }
    }

    // XXX: Lots of other stuff isn't properly destroyed here
    cairo_surface_destroy(surface);
    return 0;
}

Edit: Also, why exactly do you feel like cairo only gives you a woefully incomplete guide? It tells you how to get the cairo parts working and it also explains you some parts about X11, even though you should already know those if you want to use cairo-x11. That's none of its business. The guide you linked to even provides a complete, working and self-contained example: https://www.cypherpunk.at/files/2014/11/cairo_xlib_simple.c

1
votes

I've you would have read the complete text of this "imcomplete guide" you would have seen that there is a link to the full sample: https://www.cypherpunk.at/files/2014/11/cairo_xlib_simple.c .