Custom widgets in GTK 4 – Drawing

(This is the second part of a series about custom widgets in GTK 4. Part 1).

Drawing the old-fashioned way

Before looking at how widgets do their own drawing, it is worth pointing out that GtkDrawingArea is still a valid option if all you need is some self-contained cairo drawing.

The only difference between GTK 3 and GTK 4 is that you call gtk_drawing_area_set_draw_func() to provide your drawing function instead of connecting a signal handler to the the ::draw signal. Everything else is the same: GTK provides you with a cairo context, and you can just draw to it.

void
draw_func (GtkDrawingArea *da,
           cairo_t        *cr,
           int             width,
           int             height,
           gpointer        data)
{
  GdkRGBA red, green, yellow, blue;
  double w, h;

  w = width / 2.0;
  h = height / 2.0;

  gdk_rgba_parse (&red, "red");
  gdk_rgba_parse (&green, "green");
  gdk_rgba_parse (&yellow, "yellow");
  gdk_rgba_parse (&blue, "blue");

  gdk_cairo_set_source_rgba (cr, &red);
  cairo_rectangle (cr, 0, 0, w, h);
  cairo_fill (cr);

  gdk_cairo_set_source_rgba (cr, &green);
  cairo_rectangle (cr, w, 0, w, h);
  cairo_fill (cr);

  gdk_cairo_set_source_rgba (cr, &yellow);
  cairo_rectangle (cr, 0, h, w, h);
  cairo_fill (cr);

  gdk_cairo_set_source_rgba (cr, &blue);
  cairo_rectangle (cr, w, h, w, h);
  cairo_fill (cr);
}

...

gtk_drawing_area_set_draw_func (area, draw, NULL, NULL);

The rendering model

One of the major differences between GTK 3 and GTK 4 is that we are now targeting GL / Vulkan instead of cairo. As part of this switch, we have moved from an immediate mode rendering model to a retained mode one. In GTK 3, we were using cairo commands to render onto a surface. In GTK 4, we create a scene graph that contains render nodes, and those render nodes can be passed to renderer, or processed in some other way, or saved to a file.

In the widget API, this change is reflected in the difference between

gboolean (* draw) (GtkWidget *widget, cairo_t *cr)

and

 void (* snapshot) (GtkWidget *widget, GtkSnapshot *snapshot)

GtkSnapshot is an auxiliary object that turns your drawing commands into render nodes and adds them to the scene graph.

The CSS style information for a widget describes how to render its background, border, and so on. GTK translates this a series of function calls that add suitable render nodes to the scene graph, before and after the render nodes for the widgets’ content. So your widget automatically complies with the CSS drawing model, without any extra work.

Providing the render nodes for the content is the reponsibility of the widgets snapshot() implementation. GtkSnapshot has convenience API to make it easy.  For example, use gtk_snapshot_append_texture() to render a texture. Use gtk_snapshot_append_layout() to render text. If you want to use custom cairo drawing, gtk_snapshot_append_cairo() lets you do so.

A drawing widget

To implement a  widget that does some custom drawing, you need to implement the snapshot() function that creates the render nodes for your drawing:

void
demo_snapshot (GtkWidget *widget, GtkSnapshot *snapshot)
{
  GdkRGBA red, green, yellow, blue;
  float w, h;

  gdk_rgba_parse (&red, "red");
  gdk_rgba_parse (&green, "green");
  gdk_rgba_parse (&yellow, "yellow");
  gdk_rgba_parse (&blue, "blue");

  w = gtk_widget_get_width (widget) / 2.0;
  h = gtk_widget_get_height (widget) / 2.0;

  gtk_snapshot_append_color (snapshot, &red,
                             &GRAPHENE_RECT_INIT(0, 0, w, h));
  gtk_snapshot_append_color (snapshot, &green,
                             &GRAPHENE_RECT_INIT(w, 0, w, h));
  gtk_snapshot_append_color (snapshot, &yellow,
                             &GRAPHENE_RECT_INIT(0, h, w, h));
  gtk_snapshot_append_color (snapshot, &blue,
                             &GRAPHENE_RECT_INIT(w, h, w, h));
}

...

widget_class->snapshot = demo_snapshot;

This example produces four color nodes:

If your drawing needs a certain size, you should implement the measure() function too:

void
demo_measure (GtkWidget      *widget,
              GtkOrientation  orientation,
              int             for_size,
              int            *minimum_size,
              int            *natural_size,
              int            *minimum_baseline,
              int            *natural_baseline)
{
  *minimum_size = 100;
  *natural_size = 200;
}

...

widget_class->measure = demo_measure;

GTK keeps the render nodes produced by your snapshot() function and reuses them until you tell it that your widget needs to be drawn again by calling gdk_widget_queue_draw().

Going deeper

The GTK documentation has an overview of the GTK drawing model, if you are interested in reading more about this topic.

Outlook

In the next post, we’ll look how widgets in GTK 4 handle child widgets.