Pango, CSS and Application

PangoFontDescription

PangoFontDescription is a C structure for a font. You can get font family, style, weight and size. You can also get a string that includes font attributes. For example, suppose that the PangoFontDescription has a font of “Noto Sans Mono”, “Bold”, “Italic” and 12 points of size. Then the string converted from the PangoFontDescription is “Noto Sans Mono Bold Italic 12”.

The font in CSS is different from the string from PangoFontDescription.

So, it may be easier to use each property, i.e. font-family, font-style, font-weight and font-size, to convert a PangoFontDescription data to CSS.

Refer to Pango document and W3C CSS Fonts Module Level 3 for further information.

Converter from PangoFontDescription to CSS

Two files pfd2css.h and pfd2css.c include the converter from PangoFontDescription to CSS.

#pragma once

#include <pango/pango.h>

// Pango font description to CSS style string
// Returned string is owned by the caller. The caller should free it when it becomes useless.

char*
pfd2css (PangoFontDescription *pango_font_desc);

// Each element (family, style, weight and size)

const char*
pfd2css_family (PangoFontDescription *pango_font_desc);

const char*
pfd2css_style (PangoFontDescription *pango_font_desc);

int
pfd2css_weight (PangoFontDescription *pango_font_desc);

// Returned string is owned by the caller. The caller should free it when it becomes useless.
char *
pfd2css_size (PangoFontDescription *pango_font_desc);

The five functions are public. The first function is a convenient function to set other four CSS at once.

#include <pango/pango.h>
#include "pfd2css.h"

// Pango font description to CSS style string
// Returned string is owned by caller. The caller should free it when it is useless.

char*
pfd2css (PangoFontDescription *pango_font_desc) {
  char *fontsize;

  fontsize = pfd2css_size (pango_font_desc);
  return g_strdup_printf ("font-family: \"%s\"; font-style: %s; font-weight: %d; font-size: %s;",
              pfd2css_family (pango_font_desc), pfd2css_style (pango_font_desc),
              pfd2css_weight (pango_font_desc), fontsize);
  g_free (fontsize); 
}

// Each element (family, style, weight and size)

const char*
pfd2css_family (PangoFontDescription *pango_font_desc) {
  return pango_font_description_get_family (pango_font_desc);
}

const char*
pfd2css_style (PangoFontDescription *pango_font_desc) {
  PangoStyle pango_style = pango_font_description_get_style (pango_font_desc);
  switch (pango_style) {
  case PANGO_STYLE_NORMAL:
    return "normal";
  case PANGO_STYLE_ITALIC:
    return "italic";
  case PANGO_STYLE_OBLIQUE:
    return "oblique";
  default:
    return "normal";
  }
}

int
pfd2css_weight (PangoFontDescription *pango_font_desc) {
  PangoWeight pango_weight = pango_font_description_get_weight (pango_font_desc);
  switch (pango_weight) {
  case PANGO_WEIGHT_THIN:
    return 100;
  case PANGO_WEIGHT_ULTRALIGHT:
    return 200;
  case PANGO_WEIGHT_LIGHT:
    return 300;
  case PANGO_WEIGHT_SEMILIGHT:
    return 350;
  case PANGO_WEIGHT_BOOK:
    return 380;
  case PANGO_WEIGHT_NORMAL:
    return 400; /* or "normal" */
  case PANGO_WEIGHT_MEDIUM:
    return 500;
  case PANGO_WEIGHT_SEMIBOLD:
    return 600;
  case PANGO_WEIGHT_BOLD:
    return 700; /* or "bold" */
  case PANGO_WEIGHT_ULTRABOLD:
    return 800;
  case PANGO_WEIGHT_HEAVY:
    return 900;
  case PANGO_WEIGHT_ULTRAHEAVY:
    return 900; /* 1000 is available since CSS Fonts level 4 but GTK currently supports level 3. */
  default:
    return 400; /* "normal" */
  }
}

char *
pfd2css_size (PangoFontDescription *pango_font_desc) {
  if (pango_font_description_get_size_is_absolute (pango_font_desc))
    return g_strdup_printf ("%dpx", pango_font_description_get_size (pango_font_desc) / PANGO_SCALE);
  else
    return g_strdup_printf ("%dpt", pango_font_description_get_size (pango_font_desc) / PANGO_SCALE);
}

The font weight number is one of:

Application object

TfeApplication class

TfeApplication class is a child of GtkApplication. It has some instance variables. The header file defines the type macro and a public function.

#pragma once

#include <gtk/gtk.h>

#define TFE_TYPE_APPLICATION tfe_application_get_type ()
G_DECLARE_FINAL_TYPE (TfeApplication, tfe_application, TFE, APPLICATION, GtkApplication)

TfeApplication *
tfe_application_new (const char* application_id, GApplicationFlags flag);

The following code is extracted from tfeapplication.c. It builds TfeApplication class and instance.

#include <gtk/gtk.h>
#include "tfeapplication.h"

struct _TfeApplication {
  GtkApplication parent;
  TfeWindow *win;
  GSettings *settings;
  GtkCssProvider *provider;
};

G_DEFINE_FINAL_TYPE (TfeApplication, tfe_application, GTK_TYPE_APPLICATION)

static void
tfe_application_dispose (GObject *gobject) {
  TfeApplication *app = TFE_APPLICATION (gobject);

  g_clear_object (&app->settings);
  g_clear_object (&app->provider);
  G_OBJECT_CLASS (tfe_application_parent_class)->dispose (gobject);
}

static void
tfe_application_init (TfeApplication *app) {
  app->settings = g_settings_new ("com.github.ToshioCP.tfe");
  g_signal_connect (app->settings, "changed::font-desc", G_CALLBACK (changed_font_cb), app);
  app->provider = gtk_css_provider_new ();
}

static void
tfe_application_class_init (TfeApplicationClass *class) {
  G_OBJECT_CLASS (class)->dispose = tfe_application_dispose;
  G_APPLICATION_CLASS (class)->startup = app_startup;
  G_APPLICATION_CLASS (class)->activate = app_activate;
  G_APPLICATION_CLASS (class)->open = app_open;
}

TfeApplication *
tfe_application_new (const char* application_id, GApplicationFlags flag) {
  return TFE_APPLICATION (g_object_new (TFE_TYPE_APPLICATION, "application-id", application_id, "flags", flag, NULL));
}

Startup signal handlers

static void
app_startup (GApplication *application) {
  TfeApplication *app = TFE_APPLICATION (application);
  int i;
  GtkCssProvider *provider = gtk_css_provider_new ();
  GdkDisplay *display;

  G_APPLICATION_CLASS (tfe_application_parent_class)->startup (application);

  app->win = TFE_WINDOW (tfe_window_new (GTK_APPLICATION (app)));

  gtk_css_provider_load_from_data (provider, "textview {padding: 10px;}", -1);
  display = gdk_display_get_default ();
  gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (provider),
                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  g_object_unref (provider);
  gtk_style_context_add_provider_for_display (display, GTK_STYLE_PROVIDER (app->provider),
                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);

  changed_font_cb (app->settings, "font-desc", app); // Sets the text view font to the font from the gsettings data base.

/* ----- accelerator ----- */ 
  struct {
    const char *action;
    const char *accels[2];
  } action_accels[] = {
    { "win.open", { "<Control>o", NULL } },
    { "win.save", { "<Control>s", NULL } },
    { "win.close", { "<Control>w", NULL } },
    { "win.new", { "<Control>n", NULL } },
    { "win.saveas", { "<Shift><Control>s", NULL } },
    { "win.close-all", { "<Control>q", NULL } },
  };

  for (i = 0; i < G_N_ELEMENTS(action_accels); i++)
    gtk_application_set_accels_for_action(GTK_APPLICATION(app), action_accels[i].action, action_accels[i].accels);
}

The function app_startup replace the default signal handlers. It does five things.

Activate and open signal handlers

Two functions app_activate and app_open replace the default signal handlers.

static void
app_activate (GApplication *application) {
  TfeApplication *app = TFE_APPLICATION (application);

  tfe_window_notebook_page_new (app->win);
  gtk_window_present (GTK_WINDOW (app->win));
}

static void
app_open (GApplication *application, GFile ** files, gint n_files, const gchar *hint) {
  TfeApplication *app = TFE_APPLICATION (application);

  tfe_window_notebook_page_new_with_files (app->win, files, n_files);
  gtk_window_present (GTK_WINDOW (app->win));
}

The original default handlers don’t do useful works and you don’t need to chain up to the parent’s default handlers. They just create notebook pages and show the top level window.

CSS font setting

static void
changed_font_cb (GSettings *settings, char *key, gpointer user_data) {
  TfeApplication *app = TFE_APPLICATION (user_data);
  char *font, *s, *css;
  PangoFontDescription *pango_font_desc;

  if (g_strcmp0(key, "font-desc") != 0)
    return;
  font = g_settings_get_string (app->settings, "font-desc");
  pango_font_desc = pango_font_description_from_string (font);
  g_free (font);
  s = pfd2css (pango_font_desc); // converts Pango Font Description into CSS style string
  pango_font_description_free (pango_font_desc);
  css = g_strdup_printf ("textview {%s}", s);
  gtk_css_provider_load_from_data (app->provider, css, -1);
  g_free (s);
  g_free (css);
}

The function changed_font_cb is a handler for “changed::font-desc” signal on the GSettings instance. The signal name is “changed” and “font-desc” is a key name. This signal is emitted when the value of the “font-desc” key is changed. The value is bound to the “font-desc” property of the GtkFontDialogButton instance. Therefore, the handler changed_font_cb is called when the user selects and updates a font through the font dialog.

A string is retrieved from the GSetting database and converts it into a pango font description. And a CSS string is made by the function pfd2css and g_strdup_printf. Then the css provider is set to the string. The provider has been inserted to the current display in advance. So, the font is applied to the display.

Other files

main.c

#include <gtk/gtk.h>
#include "tfeapplication.h"

#define APPLICATION_ID "com.github.ToshioCP.tfe"

int
main (int argc, char **argv) {
  TfeApplication *app;
  int stat;

  app = tfe_application_new (APPLICATION_ID, G_APPLICATION_HANDLES_OPEN);
  stat =g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);
  return stat;
}

Resource XML file.

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/com/github/ToshioCP/tfe">
    <file>tfewindow.ui</file>
    <file>tfepref.ui</file>
    <file>tfealert.ui</file>
    <file>menu.ui</file>
  </gresource>
</gresources>

GSchema XML file

<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
  <schema path="/com/github/ToshioCP/tfe/" id="com.github.ToshioCP.tfe">
    <key name="font-desc" type="s">
      <default>'Monospace 12'</default>
      <summary>Font</summary>
      <description>A font to be used for textview.</description>
    </key>
  </schema>
</schemalist>

Meson.build

project('tfe', 'c', license : 'GPL-3.0-or-later', meson_version:' >=1.0.1', version: '0.5')

gtkdep = dependency('gtk4')

gnome = import('gnome')
resources = gnome.compile_resources('resources','tfe.gresource.xml')
gnome.compile_schemas(depend_files: 'com.github.ToshioCP.tfe.gschema.xml')

sourcefiles = files('main.c', 'tfeapplication.c', 'tfewindow.c', 'tfepref.c', 'tfealert.c', 'pfd2css.c', '../tfetextview/tfetextview.c')

executable(meson.project_name(), sourcefiles, resources, dependencies: gtkdep, export_dynamic: true, install: true)

schema_dir = get_option('prefix') / get_option('datadir') / 'glib-2.0/schemas/'
install_data('com.github.ToshioCP.tfe.gschema.xml', install_dir: schema_dir)
gnome.post_install (glib_compile_schemas: true)

Compilation and installation.

If you want to install it to your local area, use --prefix=$HOME/.local or --prefix=$HOME option. If you want to install it to the system area, no option is needed. It will be installed under /user/local directory.

$ meson setup --prefix=$HOME/.local _build
$ ninja -C _build
$ ninja -C _build install

You need root privilege to install it to the system area..

$ meson setup _build
$ ninja -C _build
$ sudo ninja -C _build install

Source files are in src/tfe6 directory.

We made a very small text editor. You can add features to this editor. When you add a new feature, be careful about the structure of the program. Maybe you need to divide a file into several files. It isn’t good to put many things into one file. And it is important to think about the relationship between source files and widget structures.

The source files are in the Gtk4 tutorial GitHub repository. Download it and see src/tfe6 directory.

Note: When the menu button is clicked, error messages are printed.

(tfe:31153): Gtk-CRITICAL **: 13:05:40.746: _gtk_css_corner_value_get_x: assertion 'corner->class == &GTK_CSS_VALUE_CORNER' failed

I found a message in the GNOME Discourse. The comment says that GTK 4.10 has a bug and it is fixed in the version 4.10.5. I haven’t check 4.10.5 yet, where the UBUNTU GTK4 is still 4.10.4.