Search code examples
clinuxx11cairo

Cairo C program won't draw to x11 window


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!

Solution

  • 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