0081: Notebook V – Customized Tabs Part 3

Okay, let’s have another look at what we’re aiming for:

Results of this example:
Current example output
Current example output
Current example terminal output
Current example terminal output (click for enlarged view)

A Second Look at the TabDrawingArea Preamble

This time around, we’ll concentrate on just the stuff we’re using to draw:

cairo_text_extents_t extents;
GtkAllocation size; // the area assigned to the DrawingArea by its parent
double[] northWestArc, northEastArc;
double xRight, xLeft, textBaseline = 22, yUpper;
double width, height;
double[] tabOutlineRGBA = [0.5, 0.5, 0.5, 1.0];
double[] tabFillRGBA = [0.949, 0.949, 0.949, 1.0];
double cornerRadius = 10, radians;
double lineWidth = 2;

The first two variables we looked at last time, but I’m repeating them here because they factor into the drawing. They are:

  • cairo_text_extents_t extents: which gives us the amount of space (width and height) that our string will take up in the DrawingArea, and
  • GtkAllocation size: which will tell us the size of the entire DrawingArea.

The extents we need so we can make the DrawingArea large enough to fit the drawing. We need the size so we can center the text within the tab.

Now, the rest of this is for the actual drawing of the shape we’ll use as the tab’s background. Here’s a diagram I used as a guide:

Figure 1: Diagram for a round-shouldered tab
Figure 1: Diagram for a round-shouldered tab

About the diagram:

  • where the red and blue lines meet is the center point for northWestArc,
  • where the violet and gray lines meet is the center point for northEastArc,

Relating the preamble variables with the diagram:

  • the northWestArc and northEastArc arrays will store values, in radians, that denote compass points for starting and stopped the drawing of the arc,
  • xRight and xLeft are the center points of the two green circles (where the blue/red lines intersect on the left and the gray/violet lines meet on the right),
  • skipping down a bit… cornerRadius is the measurement from the center of either of the circles to the outer edge, and
  • lineWidth is how thick the line will be.

Now, let’s skip back up and go over the rest of these preamble variables:

  • textBaseline: this isn’t shown on the diagram, but it’s the y offset for where we’ll be drawing the text in relation to the top of the DrawingArea… the top being ‘0’ (see Figure 2 below),
  • yUpper is marked by the horizontal blue and violet lines on the diagram, and
  • width and height will be the dimensions of our rendered text.
Figure 2: How the Custom Tab's Baseline setting Affects Placement of the Text
Figure 2: How the Custom Tab's Baseline setting Affects Placement of the Text

That’s it for the variables controlling the mechanics of the drawing, but we have two more arrays to look at (both are colors given as components of red, green, blue, and alpha):

  • tabOutlineRGBA: this is the line color, and
  • tabFillRGBA: this is an optional fill color if we want the tab to be a color other than gray.

Okay, moving right along…

A Second Look at the Constructor

this(string labelText, int tabNumber, Notebook notebook)
	_tabNumber = tabNumber;
	_notebook = notebook;
	_labelText = labelText ~ " " ~ _tabNumber.to!string();
	radians = PI / 180.0;

	// map out the shape ofa tab with rounded corners
	northWestArc = [180 * radians, 270 * radians]; // upper-left
	northEastArc = [-90 * radians, 0 * radians]; // upper-right
	cornerRadius = 10;

} // this()

Just ignore the first two lines (which were covered in Part 2 of this series).

We did look at the third line before, but let me refresh your memory… This converts _tabNumber to a string so it can be concatenated onto the end of _labelText. This gives us a finalized string each time a TabDrawingArea is instantiated with both text and a tab number.

From there we have:

  • radians: we could have grabbed this from the standard math library, but it’s just as simple to do it this way; it comes out to a little less than two one-hundredths which is very close to the true measurement of one radian… when compared to one degree of arc around a circle (6.28 radians = 360 degrees),
  • then we set up the start/end point coordinates (northWestArc and northEastArc) for drawing the rounded shoulders (keep in mind, as we talked about in the Cairo series, drawing in Cairo always goes clockwise), and
  • cornerRadius: as stated above, this is the distance starting from where the blue and red lines intersect and going to the outer edge of the arc.

The last two lines, where the signals are hooked up, we covered last time.

Finally, the onDraw() Callback

The first two lines:

bool onDraw(Scoped!Context context, Widget w)
	// set the font size to something resembling the default UI font
	context.selectFontFace("Sans 10", CairoFontSlant.NORMAL, CairoFontWeight.BOLD);

These set up the font. I picked Sans 10 because it either is the standard Windows 10 UI font or looks enough like it for our purposes. And, yes, it’s got to be bold or it just doesn’t look heavy enough compared to other UI text.

In Linux and other UNIX-like OSs, the UI font will vary depending on your theme.

Next, we have:


This gives us the size of the DrawingArea which we’ll use in a moment to figure out where to place the text.

context.textExtents(_labelText, &extents);
width = extents.width + 20;
height = extents.height + 10;

A call to textExtents() gives us the area that will be taken up by the rendered text, in pixels. We tack on a bit more in each direction so the text won’t look cramped.

setSizeRequest(cast(int)width, cast(int)height);
xLeft = 12;
xRight = width - 11;
yUpper = 15;

Here, we:

  • resize the DrawingArea so we have enough room to do the job at hand,
  • xLeft: set the x coordinate of the point where we’ll start drawing. This is 12 pixels in from the left edge of the DrawingArea,
  • xRight: set the x coordinate where we finish the drawing on the right hand side (offset 11 pixels in from the right edge,
  • yUpper: set the y location of the center of the circle (as we talked about in the preamble section above), and
  • setLineWidth(): self explanitory.

Note: You might think both xLeft and xRight should have the same value, but because of a Cairo anomaly related to line width, they don’t.

Final Setup Before Drawing the Tab

context.moveTo(2, 32);
context.lineTo(2, 12);
context.arc(xLeft,  yUpper, cornerRadius, northWestArc[0], northWestArc[1]); // upper left corner
context.arc(xRight, yUpper, cornerRadius, northEastArc[0], northEastArc[1]); // upper right corner
context.lineTo(width - 2, 32);
// context.closePath(); // closing the path puts a line across the bottom which doesn't look very nice

These statements prep the Cairo drawing routines, outlining the round-shouldered tab starting in the bottom left corner, moving up into the left shoulder, across to the right, then back down to the bottom.

Note: If you want a line across the bottom of the tab, uncomment the closePath() statement.

Note 2: Keep in mind that the above statements don’t actual do the drawing. It’s like handing someone a map with a route marked on it. The map will be used in the next step.

Drawing the Tab

First we fill it:

context.setSourceRgba(tabFillRGBA[0], tabFillRGBA[1], tabFillRGBA[2], tabFillRGBA[3]);

Then we do the outline:

context.setSourceRgba(tabOutlineRGBA[0], tabOutlineRGBA[1], tabOutlineRGBA[2], tabOutlineRGBA[3]);

And lastly, we overlay the text:

context.moveTo(size.width / 2 - extents.width / 2, textBaseline); context.showText(_labelText);

And that is that.


Next time, we’ll start by looking at adding and removing tabs from a Notebook.

Until then, have fun.

Comments? Questions? Observations?

Did we miss a tidbit of information that would make this post even more informative? Let's talk about it in the comments.

You can also subscribe via RSS so you won't miss anything. Thank you very much for dropping by.

© Copyright 2023 Ron Tarrant