[gtk/pango2] Make a standalone font explorer



commit f7c4b26c599b861bf60a5bc0d397028121f09cd0
Author: Matthias Clasen <mclasen redhat com>
Date:   Mon Jul 4 12:49:54 2022 -0400

    Make a standalone font explorer
    
    This is the font features demo, broken out as
    a standalone application and cleaned up.

 demos/font-explorer/font_features.c            | 1959 ++++++++++++++++++++++++
 demos/font-explorer/fontcolors.c               |  287 ++++
 demos/font-explorer/fontcolors.h               |   16 +
 demos/font-explorer/fontcolors.ui              |   25 +
 demos/font-explorer/fontcontrols.c             |  271 ++++
 demos/font-explorer/fontcontrols.h             |   16 +
 demos/font-explorer/fontcontrols.ui            |  175 +++
 demos/font-explorer/fontexplorer.css           |   20 +
 demos/font-explorer/fontexplorer.gresource.xml |   14 +
 demos/font-explorer/fontexplorerapp.c          |  165 ++
 demos/font-explorer/fontexplorerapp.h          |   15 +
 demos/font-explorer/fontexplorerwin.c          |  131 ++
 demos/font-explorer/fontexplorerwin.h          |   16 +
 demos/font-explorer/fontexplorerwin.ui         |  100 ++
 demos/font-explorer/fontfeatures.c             |  727 +++++++++
 demos/font-explorer/fontfeatures.h             |   16 +
 demos/font-explorer/fontfeatures.ui            |   25 +
 demos/font-explorer/fontvariations.c           |  488 ++++++
 demos/font-explorer/fontvariations.h           |   16 +
 demos/font-explorer/fontvariations.ui          |   25 +
 demos/font-explorer/fontview.c                 |  413 +++++
 demos/font-explorer/fontview.h                 |   15 +
 demos/font-explorer/fontview.ui                |   91 ++
 demos/font-explorer/language-names.c           |  336 ++++
 demos/font-explorer/language-names.h           |   13 +
 demos/font-explorer/main.c                     |    8 +
 demos/font-explorer/meson.build                |   27 +
 demos/font-explorer/rangeedit.c                |  173 +++
 demos/font-explorer/rangeedit.h                |   15 +
 demos/font-explorer/rangeedit.ui               |   24 +
 demos/font-explorer/samplechooser.c            |  162 ++
 demos/font-explorer/samplechooser.h            |   15 +
 demos/font-explorer/samplechooser.ui           |   46 +
 demos/meson.build                              |    1 +
 34 files changed, 5846 insertions(+)
---
diff --git a/demos/font-explorer/font_features.c b/demos/font-explorer/font_features.c
new file mode 100644
index 0000000000..428a27e700
--- /dev/null
+++ b/demos/font-explorer/font_features.c
@@ -0,0 +1,1959 @@
+/* Pango/Font Explorer
+ *
+ * This example demonstrates support for OpenType font features with
+ * Pango attributes. The attributes can be used manually or via Pango
+ * markup.
+ *
+ * It can also be used to explore available features in OpenType fonts
+ * and their effect.
+ *
+ * If the selected font supports OpenType font variations, then the
+ * axes are also offered for customization.
+ */
+
+#include <gtk/gtk.h>
+#include <hb.h>
+#include <hb-ot.h>
+#include <glib/gi18n.h>
+
+#include "open-type-layout.h"
+#include "fontplane.h"
+#include "script-names.h"
+#include "language-names.h"
+
+
+#define MAKE_TAG(a,b,c,d) (unsigned int)(((a) << 24) | ((b) << 16) | ((c) <<  8) | (d))
+
+typedef struct {
+  unsigned int tag;
+  const char *name;
+  GtkWidget *icon;
+  GtkWidget *dflt;
+  GtkWidget *feat;
+} FeatureItem;
+
+typedef struct {
+  unsigned int start;
+  unsigned int end;
+  Pango2FontDescription *desc;
+  char *features;
+  char *palette;
+  Pango2Language *language;
+} Range;
+
+typedef struct {
+  guint32 tag;
+  GtkAdjustment *adjustment;
+  double default_value;
+  guint tick_cb;
+  guint64 start_time;
+  gboolean increasing;
+  GtkWidget *button;
+} Axis;
+
+typedef struct {
+  GtkWidget *the_label;
+  GtkWidget *settings;
+  GtkWidget *description;
+  GtkWidget *font;
+  GtkWidget *script_lang;
+  GtkWidget *feature_list;
+  GtkWidget *variations_grid;
+  GtkWidget *colors_grid;
+  GtkWidget *first_palette;
+  GtkWidget *instance_combo;
+  GtkWidget *stack;
+  GtkWidget *entry;
+  GtkWidget *plain_toggle;
+  GtkWidget *waterfall_toggle;
+  GtkWidget *edit_toggle;
+  GtkAdjustment *size_adjustment;
+  GtkAdjustment *letterspacing_adjustment;
+  GtkAdjustment *line_height_adjustment;
+  GtkWidget *foreground;
+  GtkWidget *background;
+  GtkWidget *size_scale;
+  GtkWidget *size_entry;
+  GtkWidget *letterspacing_entry;
+  GtkWidget *line_height_entry;
+  GList *feature_items;
+  GList *ranges;
+  GHashTable *instances;
+  GHashTable *axes;
+  char *text;
+  GtkWidget *swin;
+  GtkCssProvider *provider;
+  int sample;
+  int palette;
+} FontFeaturesDemo;
+
+static void
+demo_free (gpointer data)
+{
+  FontFeaturesDemo *demo = data;
+
+  g_list_free_full (demo->feature_items, g_free);
+  g_list_free_full (demo->ranges, g_free);
+  g_clear_pointer (&demo->instances, g_hash_table_unref);
+  g_clear_pointer (&demo->axes, g_hash_table_unref);
+  g_clear_pointer (&demo->text, g_free);
+
+  g_free (demo);
+}
+
+static FontFeaturesDemo *demo;
+
+static void update_display (void);
+
+static void
+font_features_toggle_plain (void)
+{
+  if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (demo->plain_toggle)) ||
+      gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (demo->waterfall_toggle)))
+    {
+      gtk_stack_set_visible_child_name (GTK_STACK (demo->stack), "label");
+      update_display ();
+    }
+}
+
+static void
+font_features_notify_waterfall (void)
+{
+  gboolean can_change_size;
+
+  can_change_size = !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (demo->waterfall_toggle));
+  gtk_widget_set_sensitive (demo->size_scale, can_change_size);
+  gtk_widget_set_sensitive (demo->size_entry, can_change_size);
+}
+
+typedef struct {
+  GtkAdjustment *adjustment;
+  GtkEntry *entry;
+} BasicData;
+
+static gboolean
+update_in_idle (gpointer data)
+{
+  BasicData *bd = data;
+  char *str;
+
+  str = g_strdup_printf ("%g", gtk_adjustment_get_value (bd->adjustment));
+  gtk_editable_set_text (GTK_EDITABLE (bd->entry), str);
+  g_free (str);
+
+  update_display ();
+
+  g_free (bd);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+basic_value_changed (GtkAdjustment *adjustment,
+                     gpointer       data)
+{
+  BasicData *bd;
+
+  bd = g_new (BasicData, 1);
+  bd->adjustment = adjustment;
+  bd->entry = GTK_ENTRY (data);
+
+  g_idle_add (update_in_idle, bd);
+}
+
+static void
+basic_entry_activated (GtkEntry *entry,
+                       gpointer  data)
+{
+  GtkAdjustment *adjustment = data;
+
+  double value;
+  char *err = NULL;
+
+  value = g_strtod (gtk_editable_get_text (GTK_EDITABLE (entry)), &err);
+  if (err != NULL)
+    gtk_adjustment_set_value (adjustment, value);
+}
+
+static void
+color_set_cb (void)
+{
+  update_display ();
+}
+
+static void
+swap_colors (void)
+{
+  GdkRGBA fg;
+  GdkRGBA bg;
+
+  gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (demo->foreground), &fg);
+  gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (demo->background), &bg);
+  gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (demo->foreground), &bg);
+  gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (demo->background), &fg);
+}
+
+static void
+font_features_reset_basic (void)
+{
+  gtk_adjustment_set_value (demo->size_adjustment, 20);
+  gtk_adjustment_set_value (demo->letterspacing_adjustment, 0);
+  gtk_adjustment_set_value (demo->line_height_adjustment, 1);
+  gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (demo->foreground), &(GdkRGBA){0.,0.,0.,1.});
+  gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (demo->background), &(GdkRGBA){1.,1.,1.,1.});
+}
+
+static void
+update_basic (void)
+{
+  Pango2FontDescription *desc;
+
+  desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (demo->font));
+
+  gtk_adjustment_set_value (demo->size_adjustment,
+                            pango2_font_description_get_size (desc) / (double) PANGO2_SCALE);
+}
+
+static void add_font_variations (GString *s);
+
+static void
+free_range (gpointer data)
+{
+  Range *range = data;
+
+  if (range->desc)
+    pango2_font_description_free (range->desc);
+  g_free (range->features);
+  g_free (range->palette);
+  g_free (range);
+}
+
+static int
+compare_range (gconstpointer a, gconstpointer b)
+{
+  const Range *ra = a;
+  const Range *rb = b;
+
+  if (ra->start < rb->start)
+    return -1;
+  else if (ra->start > rb->start)
+    return 1;
+  else if (ra->end < rb->end)
+    return 1;
+  else if (ra->end > rb->end)
+    return -1;
+
+  return 0;
+}
+
+static void
+ensure_range (unsigned int          start,
+              unsigned int          end,
+              Pango2FontDescription *desc,
+              const char           *features,
+              const char           *palette,
+              Pango2Language        *language)
+{
+  GList *l;
+  Range *range;
+
+  for (l = demo->ranges; l; l = l->next)
+    {
+      Range *r = l->data;
+
+      if (r->start == start && r->end == end)
+        {
+          range = r;
+          goto set;
+        }
+    }
+
+  range = g_new0 (Range, 1);
+  range->start = start;
+  range->end = end;
+
+  demo->ranges = g_list_insert_sorted (demo->ranges, range, compare_range);
+
+set:
+  if (range->desc)
+    pango2_font_description_free (range->desc);
+  if (desc)
+    range->desc = pango2_font_description_copy (desc);
+  g_free (range->features);
+  range->features = g_strdup (features);
+  range->palette = g_strdup (palette);
+  range->language = language;
+}
+
+static char *
+get_feature_display_name (unsigned int tag)
+{
+  int i;
+  static char buf[5] = { 0, };
+
+  if (tag == MAKE_TAG ('x', 'x', 'x', 'x'))
+    return g_strdup (_("Default"));
+
+  hb_tag_to_string (tag, buf);
+  if (g_str_has_prefix (buf, "ss") && g_ascii_isdigit (buf[2]) && g_ascii_isdigit (buf[3]))
+    {
+      int num = (buf[2] - '0') * 10 + (buf[3] - '0');
+      return g_strdup_printf (g_dpgettext2 (NULL, "OpenType layout", "Stylistic Set %d"), num);
+    }
+  else if (g_str_has_prefix (buf, "cv") && g_ascii_isdigit (buf[2]) && g_ascii_isdigit (buf[3]))
+    {
+      int num = (buf[2] - '0') * 10 + (buf[3] - '0');
+      return g_strdup_printf (g_dpgettext2 (NULL, "OpenType layout", "Character Variant %d"), num);
+    }
+
+  for (i = 0; i < G_N_ELEMENTS (open_type_layout_features); i++)
+    {
+      if (tag == open_type_layout_features[i].tag)
+        return g_strdup (g_dpgettext2 (NULL, "OpenType layout", open_type_layout_features[i].name));
+    }
+
+  g_warning ("unknown OpenType layout feature tag: %s", buf);
+
+  return g_strdup (buf);
+}
+
+static void
+set_inconsistent (GtkCheckButton *button,
+                  gboolean        inconsistent)
+{
+  gtk_check_button_set_inconsistent (GTK_CHECK_BUTTON (button), inconsistent);
+  gtk_widget_set_opacity (gtk_widget_get_first_child (GTK_WIDGET (button)), inconsistent ? 0.0 : 1.0);
+}
+
+static void
+feat_pressed (GtkGestureClick *gesture,
+              int              n_press,
+              double           x,
+              double           y,
+              GtkWidget       *feat)
+{
+  const guint button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+
+  if (button == GDK_BUTTON_PRIMARY)
+    {
+      g_signal_handlers_block_by_func (feat, feat_pressed, NULL);
+
+      if (gtk_check_button_get_inconsistent (GTK_CHECK_BUTTON (feat)))
+        {
+          set_inconsistent (GTK_CHECK_BUTTON (feat), FALSE);
+          gtk_check_button_set_active (GTK_CHECK_BUTTON (feat), TRUE);
+        }
+
+      g_signal_handlers_unblock_by_func (feat, feat_pressed, NULL);
+    }
+  else if (button == GDK_BUTTON_SECONDARY)
+    {
+      gboolean inconsistent = gtk_check_button_get_inconsistent (GTK_CHECK_BUTTON (feat));
+      set_inconsistent (GTK_CHECK_BUTTON (feat), !inconsistent);
+    }
+}
+
+static void
+feat_toggled_cb (GtkCheckButton *check_button,
+                 gpointer        data)
+{
+  set_inconsistent (check_button, FALSE);
+}
+
+static void
+add_check_group (GtkWidget   *box,
+                 const char  *title,
+                 const char **tags)
+{
+  GtkWidget *label;
+  GtkWidget *group;
+  int i;
+
+  group = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+  gtk_widget_set_halign (group, GTK_ALIGN_START);
+
+  label = gtk_label_new (title);
+  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+  gtk_widget_set_halign (label, GTK_ALIGN_START);
+  g_object_set (label, "margin-top", 10, "margin-bottom", 10, NULL);
+  gtk_widget_add_css_class (label, "heading");
+  gtk_box_append (GTK_BOX (group), label);
+
+  for (i = 0; tags[i]; i++)
+    {
+      unsigned int tag;
+      GtkWidget *feat;
+      FeatureItem *item;
+      GtkGesture *gesture;
+      char *name;
+
+      tag = hb_tag_from_string (tags[i], -1);
+
+      name = get_feature_display_name (tag);
+      feat = gtk_check_button_new_with_label (name);
+      g_free (name);
+      set_inconsistent (GTK_CHECK_BUTTON (feat), TRUE);
+
+      g_signal_connect (feat, "notify::active", G_CALLBACK (update_display), NULL);
+      g_signal_connect (feat, "notify::inconsistent", G_CALLBACK (update_display), NULL);
+      g_signal_connect (feat, "toggled", G_CALLBACK (feat_toggled_cb), NULL);
+
+      gesture = gtk_gesture_click_new ();
+      gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), GDK_BUTTON_SECONDARY);
+      g_signal_connect (gesture, "pressed", G_CALLBACK (feat_pressed), feat);
+      gtk_widget_add_controller (feat, GTK_EVENT_CONTROLLER (gesture));
+
+      gtk_box_append (GTK_BOX (group), feat);
+
+      item = g_new (FeatureItem, 1);
+      item->name = tags[i];
+      item->tag = tag;
+      item->icon = NULL;
+      item->dflt = NULL;
+      item->feat = feat;
+
+      demo->feature_items = g_list_prepend (demo->feature_items, item);
+    }
+
+  gtk_box_append (GTK_BOX (box), group);
+}
+
+static void
+add_radio_group (GtkWidget *box,
+                 const char  *title,
+                 const char **tags)
+{
+  GtkWidget *label;
+  GtkWidget *group;
+  int i;
+  GtkWidget *group_button = NULL;
+
+  group = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+  gtk_widget_set_halign (group, GTK_ALIGN_START);
+
+  label = gtk_label_new (title);
+  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+  gtk_widget_set_halign (label, GTK_ALIGN_START);
+  g_object_set (label, "margin-top", 10, "margin-bottom", 10, NULL);
+  gtk_widget_add_css_class (label, "heading");
+  gtk_box_append (GTK_BOX (group), label);
+
+  for (i = 0; tags[i]; i++)
+    {
+      unsigned int tag;
+      GtkWidget *feat;
+      FeatureItem *item;
+      char *name;
+
+      tag = hb_tag_from_string (tags[i], -1);
+      name = get_feature_display_name (tag);
+      feat = gtk_check_button_new_with_label (name ? name : _("Default"));
+      g_free (name);
+      if (group_button == NULL)
+        group_button = feat;
+      else
+        gtk_check_button_set_group (GTK_CHECK_BUTTON (feat), GTK_CHECK_BUTTON (group_button));
+
+      g_signal_connect (feat, "notify::active", G_CALLBACK (update_display), NULL);
+      g_object_set_data (G_OBJECT (feat), "default", group_button);
+
+      gtk_box_append (GTK_BOX (group), feat);
+
+      item = g_new (FeatureItem, 1);
+      item->name = tags[i];
+      item->tag = tag;
+      item->icon = NULL;
+      item->dflt = NULL;
+      item->feat = feat;
+
+      demo->feature_items = g_list_prepend (demo->feature_items, item);
+    }
+
+  gtk_box_append (GTK_BOX (box), group);
+}
+
+static void
+update_display (void)
+{
+  GString *s;
+  char *text;
+  gboolean has_feature;
+  GtkTreeIter iter;
+  GtkTreeModel *model;
+  Pango2FontDescription *desc;
+  GList *l;
+  Pango2AttrList *attrs;
+  Pango2Attribute *attr;
+  int ins, bound;
+  guint start, end;
+  Pango2Language *lang;
+  char *font_desc;
+  char *features;
+  double value;
+  int text_len;
+  gboolean do_waterfall;
+  GString *waterfall;
+  char *palette;
+
+  {
+    GtkTextBuffer *buffer;
+    GtkTextIter start_iter, end_iter;
+
+    buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (demo->entry));
+    gtk_text_buffer_get_bounds (buffer, &start_iter, &end_iter);
+    text = gtk_text_buffer_get_text (buffer, &start_iter, &end_iter, FALSE);
+    text_len = strlen (text);
+  }
+
+  do_waterfall = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (demo->waterfall_toggle));
+
+  gtk_label_set_wrap (GTK_LABEL (demo->the_label), !do_waterfall);
+
+  if (do_waterfall)
+    {
+      start = PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING;
+      end = PANGO2_ATTR_INDEX_TO_TEXT_END;
+    }
+  else if (gtk_label_get_selection_bounds (GTK_LABEL (demo->the_label), &ins, &bound))
+    {
+      start = g_utf8_offset_to_pointer (text, ins) - text;
+      end = g_utf8_offset_to_pointer (text, bound) - text;
+    }
+  else
+    {
+      start = PANGO2_ATTR_INDEX_FROM_TEXT_BEGINNING;
+      end = PANGO2_ATTR_INDEX_TO_TEXT_END;
+    }
+
+  desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (demo->font));
+
+  value = gtk_adjustment_get_value (demo->size_adjustment);
+  pango2_font_description_set_size (desc, value * PANGO2_SCALE);
+
+  s = g_string_new ("");
+  add_font_variations (s);
+  if (s->len > 0)
+    {
+      pango2_font_description_set_variations (desc, s->str);
+      g_string_free (s, TRUE);
+    }
+
+  font_desc = pango2_font_description_to_string (desc);
+
+  s = g_string_new ("");
+
+  has_feature = FALSE;
+  for (l = demo->feature_items; l; l = l->next)
+    {
+      FeatureItem *item = l->data;
+
+      if (!gtk_widget_is_sensitive (item->feat))
+        continue;
+
+      if (GTK_IS_CHECK_BUTTON (item->feat))
+        {
+          if (g_object_get_data (G_OBJECT (item->feat), "default"))
+            {
+              if (gtk_check_button_get_active (GTK_CHECK_BUTTON (item->feat)) &&
+                  strcmp (item->name, "xxxx") != 0)
+                {
+                  if (has_feature)
+                    g_string_append (s, ", ");
+                  g_string_append (s, item->name);
+                  g_string_append (s, " 1");
+                  has_feature = TRUE;
+                }
+            }
+          else
+            {
+              if (gtk_check_button_get_inconsistent (GTK_CHECK_BUTTON (item->feat)))
+                continue;
+
+              if (has_feature)
+                g_string_append (s, ", ");
+              g_string_append (s, item->name);
+              if (gtk_check_button_get_active (GTK_CHECK_BUTTON (item->feat)))
+                g_string_append (s, " 1");
+              else
+                g_string_append (s, " 0");
+              has_feature = TRUE;
+            }
+        }
+    }
+
+  features = g_string_free (s, FALSE);
+
+  palette = g_strdup_printf ("palette%d", demo->palette);
+
+  if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (demo->script_lang), &iter))
+    {
+      hb_tag_t lang_tag;
+
+      model = gtk_combo_box_get_model (GTK_COMBO_BOX (demo->script_lang));
+      gtk_tree_model_get (model, &iter, 3, &lang_tag, -1);
+
+      lang = pango2_language_from_string (hb_language_to_string (hb_ot_tag_to_language (lang_tag)));
+    }
+  else
+    lang = NULL;
+
+  attrs = pango2_attr_list_new ();
+
+  if (gtk_adjustment_get_value (demo->letterspacing_adjustment) != 0.)
+    {
+      attr = pango2_attr_letter_spacing_new (gtk_adjustment_get_value (demo->letterspacing_adjustment));
+      pango2_attribute_set_range (attr, start, end);
+      pango2_attr_list_insert (attrs, attr);
+    }
+
+  if (gtk_adjustment_get_value (demo->line_height_adjustment) != 1.)
+    {
+      attr = pango2_attr_line_height_new (gtk_adjustment_get_value (demo->line_height_adjustment));
+      pango2_attribute_set_range (attr, start, end);
+      pango2_attr_list_insert (attrs, attr);
+    }
+
+    {
+      GdkRGBA rgba;
+      char *fg, *bg, *css;
+
+      gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (demo->foreground), &rgba);
+      attr = pango2_attr_foreground_new (&(Pango2Color){ 65535 * rgba.red,
+                                                         65535 * rgba.green,
+                                                         65535 * rgba.blue,
+                                                         65535 * rgba.alpha });
+      pango2_attribute_set_range (attr, start, end);
+      pango2_attr_list_insert (attrs, attr);
+
+      fg = gdk_rgba_to_string (&rgba);
+      gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (demo->background), &rgba);
+      bg = gdk_rgba_to_string (&rgba);
+      css = g_strdup_printf (".font_features_background { caret-color: %s; background-color: %s; }", fg, bg);
+      gtk_css_provider_load_from_data (demo->provider, css, strlen (css));
+      g_free (css);
+      g_free (fg);
+      g_free (bg);
+    }
+
+  if (do_waterfall)
+    {
+      attr = pango2_attr_font_desc_new (desc);
+      pango2_attr_list_insert (attrs, attr);
+      attr = pango2_attr_font_features_new (features);
+      pango2_attr_list_insert (attrs, attr);
+      attr = pango2_attr_palette_new (palette);
+      pango2_attr_list_insert (attrs, attr);
+      attr = pango2_attr_language_new (lang);
+      pango2_attr_list_insert (attrs, attr);
+    }
+  else
+    {
+      ensure_range (start, end, desc, features, palette, lang);
+
+      for (l = demo->ranges; l; l = l->next)
+        {
+          Range *range = l->data;
+
+          attr = pango2_attr_font_desc_new (range->desc);
+          pango2_attribute_set_range (attr, range->start, range->end);
+          pango2_attr_list_insert (attrs, attr);
+
+          attr = pango2_attr_font_features_new (range->features);
+          pango2_attribute_set_range (attr, range->start, range->end);
+          pango2_attr_list_insert (attrs, attr);
+
+          attr = pango2_attr_palette_new (range->palette);
+          pango2_attribute_set_range (attr, range->start, range->end);
+          pango2_attr_list_insert (attrs, attr);
+
+          if (range->language)
+            {
+              attr = pango2_attr_language_new (range->language);
+              pango2_attribute_set_range (attr, range->start, range->end);
+              pango2_attr_list_insert (attrs, attr);
+            }
+        }
+    }
+
+  gtk_label_set_text (GTK_LABEL (demo->description), font_desc);
+  gtk_label_set_text (GTK_LABEL (demo->settings), features);
+
+  if (do_waterfall)
+    {
+      waterfall = g_string_new ("");
+      int sizes[] = { 7, 8, 9, 10, 12, 14, 16, 20, 24, 30, 40, 50, 60, 70, 90 };
+      start = 0;
+      for (int i = 0; i < G_N_ELEMENTS (sizes); i++)
+        {
+          g_string_append (waterfall, text);
+          g_string_append (waterfall, "
"); /* Unicode line separator */
+
+          attr = pango2_attr_size_new (sizes[i] * PANGO2_SCALE);
+          pango2_attribute_set_range (attr, start, start + text_len);
+          pango2_attr_list_insert (attrs, attr);
+
+          start += text_len + strlen ("
");
+        }
+      gtk_label_set_text (GTK_LABEL (demo->the_label), waterfall->str);
+      g_string_free (waterfall, TRUE);
+    }
+  else
+    gtk_label_set_text (GTK_LABEL (demo->the_label), text);
+
+  gtk_label_set_attributes (GTK_LABEL (demo->the_label), attrs);
+
+  g_free (font_desc);
+  pango2_font_description_free (desc);
+  g_free (features);
+  g_free (palette);
+  pango2_attr_list_unref (attrs);
+  g_free (text);
+}
+
+static Pango2Font *
+get_pango_font (void)
+{
+  Pango2FontDescription *desc;
+  Pango2Context *context;
+
+  desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (demo->font));
+  context = gtk_widget_get_pango_context (demo->font);
+
+  return pango2_context_load_font (context, desc);
+}
+
+typedef struct {
+  hb_tag_t script_tag;
+  hb_tag_t lang_tag;
+  unsigned int script_index;
+  unsigned int lang_index;
+} TagPair;
+
+static guint
+tag_pair_hash (gconstpointer data)
+{
+  const TagPair *pair = data;
+
+  return pair->script_tag + pair->lang_tag;
+}
+
+static gboolean
+tag_pair_equal (gconstpointer a, gconstpointer b)
+{
+  const TagPair *pair_a = a;
+  const TagPair *pair_b = b;
+
+  return pair_a->script_tag == pair_b->script_tag && pair_a->lang_tag == pair_b->lang_tag;
+}
+
+static int
+script_sort_func (GtkTreeModel *model,
+                  GtkTreeIter  *a,
+                  GtkTreeIter  *b,
+                  gpointer      user_data)
+{
+  char *sa, *sb;
+  int ret;
+
+  gtk_tree_model_get (model, a, 0, &sa, -1);
+  gtk_tree_model_get (model, b, 0, &sb, -1);
+
+  ret = strcmp (sa, sb);
+
+  g_free (sa);
+  g_free (sb);
+
+  return ret;
+}
+
+static void
+update_script_combo (void)
+{
+  GtkListStore *store;
+  hb_font_t *hb_font;
+  int i, j, k;
+  Pango2Font *pango_font;
+  GHashTable *tags;
+  GHashTableIter iter;
+  TagPair *pair;
+  char *lang;
+  hb_tag_t active;
+  GtkTreeIter active_iter;
+  gboolean have_active = FALSE;
+
+  lang = gtk_font_chooser_get_language (GTK_FONT_CHOOSER (demo->font));
+
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+  active = hb_ot_tag_from_language (hb_language_from_string (lang, -1));
+  G_GNUC_END_IGNORE_DEPRECATIONS
+
+  g_free (lang);
+
+  store = gtk_list_store_new (4, G_TYPE_STRING, G_TYPE_UINT, G_TYPE_UINT, G_TYPE_UINT);
+
+  pango_font = get_pango_font ();
+  hb_font = pango2_font_get_hb_font (pango_font);
+
+  tags = g_hash_table_new_full (tag_pair_hash, tag_pair_equal, g_free, NULL);
+
+  pair = g_new (TagPair, 1);
+  pair->script_tag = 0;
+  pair->lang_tag = 0;
+  g_hash_table_add (tags, pair);
+
+  pair = g_new (TagPair, 1);
+  pair->script_tag = HB_OT_TAG_DEFAULT_SCRIPT;
+  pair->lang_tag = HB_OT_TAG_DEFAULT_LANGUAGE;
+  g_hash_table_add (tags, pair);
+
+  if (hb_font)
+    {
+      hb_tag_t tables[2] = { HB_OT_TAG_GSUB, HB_OT_TAG_GPOS };
+      hb_face_t *hb_face;
+
+      hb_face = hb_font_get_face (hb_font);
+
+      for (i= 0; i < 2; i++)
+        {
+          hb_tag_t scripts[80];
+          unsigned int script_count = G_N_ELEMENTS (scripts);
+
+          hb_ot_layout_table_get_script_tags (hb_face, tables[i], 0, &script_count, scripts);
+          for (j = 0; j < script_count; j++)
+            {
+              hb_tag_t languages[80];
+              unsigned int language_count = G_N_ELEMENTS (languages);
+
+              hb_ot_layout_script_get_language_tags (hb_face, tables[i], j, 0, &language_count, languages);
+              for (k = 0; k < language_count; k++)
+                {
+                  pair = g_new (TagPair, 1);
+                  pair->script_tag = scripts[j];
+                  pair->lang_tag = languages[k];
+                  pair->script_index = j;
+                  pair->lang_index = k;
+                  g_hash_table_add (tags, pair);
+                }
+            }
+        }
+    }
+
+  g_object_unref (pango_font);
+
+  g_hash_table_iter_init (&iter, tags);
+  while (g_hash_table_iter_next (&iter, (gpointer *)&pair, NULL))
+    {
+      const char *langname;
+      char langbuf[5];
+      GtkTreeIter tree_iter;
+
+      if (pair->lang_tag == 0 && pair->script_tag == 0)
+        langname = NC_("Language", "None");
+      else if (pair->lang_tag == HB_OT_TAG_DEFAULT_LANGUAGE)
+        langname = NC_("Language", "Default");
+      else
+        {
+          langname = get_language_name_for_tag (pair->lang_tag);
+          if (!langname)
+            {
+              hb_tag_to_string (pair->lang_tag, langbuf);
+              langbuf[4] = 0;
+              langname = langbuf;
+            }
+        }
+
+      gtk_list_store_insert_with_values (store, &tree_iter, -1,
+                                         0, langname,
+                                         1, pair->script_index,
+                                         2, pair->lang_index,
+                                         3, pair->lang_tag,
+                                         -1);
+      if (pair->lang_tag == active)
+        {
+          have_active = TRUE;
+          active_iter = tree_iter;
+        }
+    }
+
+  g_hash_table_destroy (tags);
+
+  gtk_tree_sortable_set_default_sort_func (GTK_TREE_SORTABLE (store),
+                                           script_sort_func, NULL, NULL);
+  gtk_tree_sortable_set_sort_column_id (GTK_TREE_SORTABLE (store),
+                                        GTK_TREE_SORTABLE_DEFAULT_SORT_COLUMN_ID,
+                                        GTK_SORT_ASCENDING);
+  gtk_combo_box_set_model (GTK_COMBO_BOX (demo->script_lang), GTK_TREE_MODEL (store));
+  if (have_active)
+    gtk_combo_box_set_active_iter (GTK_COMBO_BOX (demo->script_lang), &active_iter);
+  else
+    gtk_combo_box_set_active_iter (GTK_COMBO_BOX (demo->script_lang), 0);
+}
+
+static char *
+get_name (hb_face_t       *hbface,
+          hb_ot_name_id_t  id)
+{
+  unsigned int len;
+  char *text;
+
+  if (id == HB_OT_NAME_ID_INVALID)
+    return NULL;
+
+  len = hb_ot_name_get_utf8 (hbface, id, HB_LANGUAGE_INVALID, NULL, NULL);
+  len++;
+  text = g_new (char, len);
+  hb_ot_name_get_utf8 (hbface, id, HB_LANGUAGE_INVALID, &len, text);
+
+  return text;
+}
+
+static void
+update_features (void)
+{
+  int i, j;
+  GtkTreeModel *model;
+  GtkTreeIter iter;
+  guint script_index, lang_index;
+  hb_tag_t lang_tag;
+  Pango2Font *pango_font;
+  hb_font_t *hb_font;
+  GList *l;
+
+  /* set feature presence checks from the font features */
+
+  if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (demo->script_lang), &iter))
+    return;
+
+  model = gtk_combo_box_get_model (GTK_COMBO_BOX (demo->script_lang));
+  gtk_tree_model_get (model, &iter,
+                      1, &script_index,
+                      2, &lang_index,
+                      3, &lang_tag,
+                      -1);
+
+  if (lang_tag == 0) /* None is selected */
+    {
+      for (l = demo->feature_items; l; l = l->next)
+        {
+          FeatureItem *item = l->data;
+          gtk_widget_show (item->feat);
+          gtk_widget_show (gtk_widget_get_parent (item->feat));
+          if (strcmp (item->name, "xxxx") == 0)
+            gtk_check_button_set_active (GTK_CHECK_BUTTON (item->feat), TRUE);
+        }
+
+      return;
+    }
+
+  for (l = demo->feature_items; l; l = l->next)
+    {
+      FeatureItem *item = l->data;
+      gtk_widget_hide (item->feat);
+      gtk_widget_hide (gtk_widget_get_parent (item->feat));
+      if (strcmp (item->name, "xxxx") == 0)
+        gtk_check_button_set_active (GTK_CHECK_BUTTON (item->feat), TRUE);
+    }
+
+  /* set feature presence checks from the font features */
+
+  if (!gtk_combo_box_get_active_iter (GTK_COMBO_BOX (demo->script_lang), &iter))
+    return;
+
+  model = gtk_combo_box_get_model (GTK_COMBO_BOX (demo->script_lang));
+  gtk_tree_model_get (model, &iter,
+                      1, &script_index,
+                      2, &lang_index,
+                      -1);
+
+  pango_font = get_pango_font ();
+  hb_font = pango2_font_get_hb_font (pango_font);
+
+  if (hb_font)
+    {
+      hb_tag_t tables[2] = { HB_OT_TAG_GSUB, HB_OT_TAG_GPOS };
+      hb_face_t *hb_face;
+      char *feat;
+
+      hb_face = hb_font_get_face (hb_font);
+
+      for (i = 0; i < 2; i++)
+        {
+          hb_tag_t features[80];
+          unsigned int count = G_N_ELEMENTS(features);
+
+          hb_ot_layout_language_get_feature_tags (hb_face,
+                                                  tables[i],
+                                                  script_index,
+                                                  lang_index,
+                                                  0,
+                                                  &count,
+                                                  features);
+
+          for (j = 0; j < count; j++)
+            {
+              char buf[5];
+              hb_tag_to_string (features[j], buf);
+              buf[4] = 0;
+#if 0
+              g_print ("%s present in %s\n", buf, i == 0 ? "GSUB" : "GPOS");
+#endif
+
+              if (g_str_has_prefix (buf, "ss") || g_str_has_prefix (buf, "cv"))
+                {
+                  unsigned int feature_index;
+                  hb_ot_name_id_t label_id, tooltip_id, sample_id, first_param_id;
+                  unsigned int num_params;
+
+                  hb_ot_layout_language_find_feature (hb_face,
+                                                      tables[i],
+                                                      script_index,
+                                                      lang_index,
+                                                      features[j],
+                                                      &feature_index);
+
+                  if (hb_ot_layout_feature_get_name_ids (hb_face,
+                                                         tables[i],
+                                                         feature_index,
+                                                         &label_id,
+                                                         &tooltip_id,
+                                                         &sample_id,
+                                                         &num_params,
+                                                         &first_param_id))
+                    {
+                      char *label = get_name (hb_face, label_id);
+
+                      if (label)
+                        {
+                          for (l = demo->feature_items; l; l = l->next)
+                            {
+                              FeatureItem *item = l->data;
+
+                              if (item->tag == features[j])
+                                {
+                                  gtk_check_button_set_label (GTK_CHECK_BUTTON (item->feat), label);
+                                  break;
+                                }
+                            }
+                        }
+
+                      g_free (label);
+                    }
+                }
+
+              for (l = demo->feature_items; l; l = l->next)
+                {
+                  FeatureItem *item = l->data;
+
+                  if (item->tag == features[j])
+                    {
+                      gtk_widget_show (item->feat);
+                      gtk_widget_show (gtk_widget_get_parent (item->feat));
+                      if (GTK_IS_CHECK_BUTTON (item->feat))
+                        {
+                          GtkWidget *def = GTK_WIDGET (g_object_get_data (G_OBJECT (item->feat), "default"));
+                          if (def)
+                            {
+                              gtk_widget_show (def);
+                              gtk_widget_show (gtk_widget_get_parent (def));
+                              gtk_check_button_set_active (GTK_CHECK_BUTTON (def), TRUE);
+                            }
+                          else
+                            set_inconsistent (GTK_CHECK_BUTTON (item->feat), TRUE);
+                        }
+                    }
+                }
+            }
+        }
+
+      feat = gtk_font_chooser_get_font_features (GTK_FONT_CHOOSER (demo->font));
+      if (feat)
+        {
+          for (l = demo->feature_items; l; l = l->next)
+            {
+              FeatureItem *item = l->data;
+              char buf[5];
+              char *p;
+
+              hb_tag_to_string (item->tag, buf);
+              buf[4] = 0;
+
+              p = strstr (feat, buf);
+              if (p)
+                {
+                  if (GTK_IS_CHECK_BUTTON (item->feat) && g_object_get_data (G_OBJECT (item->feat), 
"default"))
+                    {
+                      gtk_check_button_set_active (GTK_CHECK_BUTTON (item->feat), p[6] == '1');
+                    }
+                  else if (GTK_IS_CHECK_BUTTON (item->feat))
+                    {
+                      set_inconsistent (GTK_CHECK_BUTTON (item->feat), FALSE);
+                      gtk_check_button_set_active (GTK_CHECK_BUTTON (item->feat), p[6] == '1');
+                    }
+                }
+            }
+
+          g_free (feat);
+        }
+    }
+
+  g_object_unref (pango_font);
+}
+
+#define FixedToFloat(f) (((float)(f))/65536.0)
+
+static void
+adjustment_changed (GtkAdjustment *adjustment,
+                    GtkEntry      *entry)
+{
+  char *str;
+
+  str = g_strdup_printf ("%g", gtk_adjustment_get_value (adjustment));
+  gtk_editable_set_text (GTK_EDITABLE (entry), str);
+  g_free (str);
+
+  update_display ();
+}
+
+static void
+entry_activated (GtkEntry *entry,
+                 GtkAdjustment *adjustment)
+{
+  double value;
+  char *err = NULL;
+
+  value = g_strtod (gtk_editable_get_text (GTK_EDITABLE (entry)), &err);
+  if (err != NULL)
+    gtk_adjustment_set_value (adjustment, value);
+}
+
+static void unset_instance (GtkAdjustment *adjustment);
+
+static void start_or_stop_axis_animation (GtkButton *button,
+                                          gpointer   data);
+
+static void
+font_features_reset_variations (void)
+{
+  GHashTableIter iter;
+  Axis *axis;
+
+  g_hash_table_iter_init (&iter, demo->axes);
+  while (g_hash_table_iter_next (&iter, (gpointer *)NULL, (gpointer *)&axis))
+    {
+      if (axis->tick_cb)
+        start_or_stop_axis_animation (GTK_BUTTON (axis->button), axis);
+      gtk_adjustment_set_value (axis->adjustment, axis->default_value);
+    }
+}
+
+static void
+add_font_variations (GString *s)
+{
+  GHashTableIter iter;
+  Axis *axis;
+  char buf[G_ASCII_DTOSTR_BUF_SIZE];
+  const char *sep = "";
+
+  g_hash_table_iter_init (&iter, demo->axes);
+  while (g_hash_table_iter_next (&iter, (gpointer *)NULL, (gpointer *)&axis))
+    {
+      char tag[5];
+      double value;
+
+      hb_tag_to_string (axis->tag, tag);
+      tag[4] = '\0';
+      value = gtk_adjustment_get_value (axis->adjustment);
+
+      g_string_append_printf (s, "%s%s=%s", sep, tag, g_ascii_dtostr (buf, sizeof (buf), value));
+      sep = ",";
+    }
+}
+
+static guint
+axes_hash (gconstpointer v)
+{
+  const Axis *p = v;
+
+  return p->tag;
+}
+
+static gboolean
+axes_equal (gconstpointer v1, gconstpointer v2)
+{
+  const Axis *p1 = v1;
+  const Axis *p2 = v2;
+
+  return p1->tag == p2->tag;
+}
+
+static double
+ease_out_cubic (double t)
+{
+  double p = t - 1;
+
+  return p * p * p + 1;
+}
+
+static const guint64 period = G_TIME_SPAN_SECOND * 3;
+
+static gboolean
+animate_axis (GtkWidget     *widget,
+              GdkFrameClock *frame_clock,
+              gpointer       data)
+{
+  Axis *axis = data;
+  guint64 now;
+  double upper, lower, value;
+
+  now = g_get_monotonic_time ();
+
+  if (now >= axis->start_time + period)
+    {
+      axis->start_time += period;
+      axis->increasing = !axis->increasing;
+    }
+
+  value = (now - axis->start_time) / (double) period;
+
+  value = ease_out_cubic (value);
+
+  lower = gtk_adjustment_get_lower (axis->adjustment);
+  upper = gtk_adjustment_get_upper (axis->adjustment);
+
+  if (axis->increasing)
+    gtk_adjustment_set_value (axis->adjustment, lower + (upper - lower) * value);
+  else
+    gtk_adjustment_set_value (axis->adjustment, upper - (upper - lower) * value);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+start_or_stop_axis_animation (GtkButton *button,
+                              gpointer   data)
+{
+  Axis *axis = data;
+
+  if (axis->tick_cb)
+    {
+      gtk_widget_remove_tick_callback (GTK_WIDGET (button), axis->tick_cb);
+      axis->tick_cb = 0;
+      gtk_button_set_icon_name (button, "media-playback-start");
+    }
+  else
+    {
+      double value, upper, lower;
+
+      gtk_button_set_icon_name (button, "media-playback-stop");
+      axis->tick_cb = gtk_widget_add_tick_callback (GTK_WIDGET (button), animate_axis, axis, NULL);
+      value = gtk_adjustment_get_value (axis->adjustment);
+      lower = gtk_adjustment_get_lower (axis->adjustment);
+      upper = gtk_adjustment_get_upper (axis->adjustment);
+      value = value / (upper - lower);
+      axis->start_time = g_get_monotonic_time () - value * period;
+      axis->increasing = TRUE;
+    }
+}
+
+static void
+add_axis (hb_face_t             *hb_face,
+          hb_ot_var_axis_info_t *ax,
+          float                  value,
+          int                    i)
+{
+  GtkWidget *axis_label;
+  GtkWidget *axis_entry;
+  GtkWidget *axis_scale;
+  GtkAdjustment *adjustment;
+  Axis *axis;
+  char name[20];
+  unsigned int name_len = 20;
+
+  hb_ot_name_get_utf8 (hb_face, ax->name_id, HB_LANGUAGE_INVALID, &name_len, name);
+
+  axis_label = gtk_label_new (name);
+  gtk_widget_set_halign (axis_label, GTK_ALIGN_START);
+  gtk_widget_set_valign (axis_label, GTK_ALIGN_BASELINE);
+  gtk_grid_attach (GTK_GRID (demo->variations_grid), axis_label, 0, i, 1, 1);
+  adjustment = gtk_adjustment_new (value, ax->min_value, ax->max_value,
+                                   1.0, 10.0, 0.0);
+  axis_scale = gtk_scale_new (GTK_ORIENTATION_HORIZONTAL, adjustment);
+  gtk_scale_add_mark (GTK_SCALE (axis_scale), ax->default_value, GTK_POS_TOP, NULL);
+  gtk_widget_set_valign (axis_scale, GTK_ALIGN_BASELINE);
+  gtk_widget_set_hexpand (axis_scale, TRUE);
+  gtk_widget_set_size_request (axis_scale, 100, -1);
+  gtk_grid_attach (GTK_GRID (demo->variations_grid), axis_scale, 1, i, 1, 1);
+  axis_entry = gtk_entry_new ();
+  gtk_widget_set_valign (axis_entry, GTK_ALIGN_BASELINE);
+  gtk_editable_set_width_chars (GTK_EDITABLE (axis_entry), 4);
+  gtk_editable_set_max_width_chars (GTK_EDITABLE (axis_entry), 4);
+  gtk_widget_set_hexpand (axis_entry, FALSE);
+  gtk_grid_attach (GTK_GRID (demo->variations_grid), axis_entry, 2, i, 1, 1);
+
+  axis = g_new0 (Axis, 1);
+  axis->tag = ax->tag;
+  axis->adjustment = adjustment;
+  axis->default_value = ax->default_value;
+  g_hash_table_add (demo->axes, axis);
+
+  axis->button = gtk_button_new_from_icon_name ("media-playback-start");
+  gtk_widget_add_css_class (GTK_WIDGET (axis->button), "circular");
+  gtk_widget_set_valign (GTK_WIDGET (axis->button), GTK_ALIGN_CENTER);
+  g_signal_connect (axis->button, "clicked", G_CALLBACK (start_or_stop_axis_animation), axis);
+  gtk_grid_attach (GTK_GRID (demo->variations_grid), axis->button, 3, i, 1, 1);
+
+  adjustment_changed (adjustment, GTK_ENTRY (axis_entry));
+
+  g_signal_connect (adjustment, "value-changed", G_CALLBACK (adjustment_changed), axis_entry);
+  g_signal_connect (adjustment, "value-changed", G_CALLBACK (unset_instance), NULL);
+  g_signal_connect (axis_entry, "activate", G_CALLBACK (entry_activated), adjustment);
+}
+
+typedef struct {
+  char *name;
+  unsigned int index;
+} Instance;
+
+static guint
+instance_hash (gconstpointer v)
+{
+  const Instance *p = v;
+
+  return g_str_hash (p->name);
+}
+
+static gboolean
+instance_equal (gconstpointer v1, gconstpointer v2)
+{
+  const Instance *p1 = v1;
+  const Instance *p2 = v2;
+
+  return g_str_equal (p1->name, p2->name);
+}
+
+static void
+free_instance (gpointer data)
+{
+  Instance *instance = data;
+
+  g_free (instance->name);
+  g_free (instance);
+}
+
+static void
+add_instance (hb_face_t    *face,
+              unsigned int  index,
+              GtkWidget    *combo,
+              int           pos)
+{
+  Instance *instance;
+  hb_ot_name_id_t name_id;
+  char name[20];
+  unsigned int name_len = 20;
+
+  instance = g_new0 (Instance, 1);
+
+  name_id = hb_ot_var_named_instance_get_subfamily_name_id (face, index);
+  hb_ot_name_get_utf8 (face, name_id, HB_LANGUAGE_INVALID, &name_len, name);
+
+  instance->name = g_strdup (name);
+  instance->index = index;
+
+  g_hash_table_add (demo->instances, instance);
+  gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), instance->name);
+}
+
+static void
+unset_instance (GtkAdjustment *adjustment)
+{
+  if (demo->instance_combo)
+    gtk_combo_box_set_active (GTK_COMBO_BOX (demo->instance_combo), 0);
+}
+
+static void
+instance_changed (GtkComboBox *combo)
+{
+  char *text;
+  Instance *instance;
+  Instance ikey;
+  int i;
+  unsigned int coords_length;
+  float *coords = NULL;
+  hb_ot_var_axis_info_t *ai = NULL;
+  unsigned int n_axes;
+  Pango2Font *pango_font = NULL;
+  hb_font_t *hb_font;
+  hb_face_t *hb_face;
+
+  text = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (combo));
+  if (text[0] == '\0')
+    goto out;
+
+  ikey.name = text;
+  instance = g_hash_table_lookup (demo->instances, &ikey);
+  if (!instance)
+    {
+      g_print ("did not find instance %s\n", text);
+      goto out;
+    }
+
+  pango_font = get_pango_font ();
+  hb_font = pango2_font_get_hb_font (pango_font);
+  hb_face = hb_font_get_face (hb_font);
+
+  n_axes = hb_ot_var_get_axis_infos (hb_face, 0, NULL, NULL);
+  ai = g_new (hb_ot_var_axis_info_t, n_axes);
+  hb_ot_var_get_axis_infos (hb_face, 0, &n_axes, ai);
+
+  coords = g_new (float, n_axes);
+  hb_ot_var_named_instance_get_design_coords (hb_face,
+                                              instance->index,
+                                              &coords_length,
+                                              coords);
+
+  for (i = 0; i < n_axes; i++)
+    {
+      Axis *axis;
+      Axis akey;
+      double value;
+
+      value = coords[ai[i].axis_index];
+
+      akey.tag = ai[i].tag;
+      axis = g_hash_table_lookup (demo->axes, &akey);
+      if (axis)
+        {
+          g_signal_handlers_block_by_func (axis->adjustment, unset_instance, NULL);
+          gtk_adjustment_set_value (axis->adjustment, value);
+          g_signal_handlers_unblock_by_func (axis->adjustment, unset_instance, NULL);
+        }
+    }
+
+out:
+  g_free (text);
+  g_clear_object (&pango_font);
+  g_free (ai);
+  g_free (coords);
+}
+
+static gboolean
+matches_instance (hb_face_t             *hb_face,
+                  unsigned int           index,
+                  unsigned int           n_axes,
+                  float                 *coords)
+{
+  float *instance_coords;
+  unsigned int coords_length;
+  int i;
+
+  instance_coords = g_new (float, n_axes);
+  coords_length = n_axes;
+
+  hb_ot_var_named_instance_get_design_coords (hb_face,
+                                              index,
+                                              &coords_length,
+                                              instance_coords);
+
+  for (i = 0; i < n_axes; i++)
+    if (instance_coords[i] != coords[i])
+      return FALSE;
+
+  return TRUE;
+}
+
+static void
+add_font_plane (int i)
+{
+  GtkWidget *plane;
+  Axis *weight_axis;
+  Axis *width_axis;
+
+  Axis key;
+
+  key.tag = MAKE_TAG('w','g','h','t');
+  weight_axis = g_hash_table_lookup (demo->axes, &key);
+  key.tag = MAKE_TAG('w','d','t','h');
+  width_axis = g_hash_table_lookup (demo->axes, &key);
+
+  if (weight_axis && width_axis)
+    {
+      plane = gtk_font_plane_new (weight_axis->adjustment,
+                                  width_axis->adjustment);
+
+      gtk_widget_set_size_request (plane, 300, 300);
+      gtk_widget_set_halign (plane, GTK_ALIGN_CENTER);
+      gtk_grid_attach (GTK_GRID (demo->variations_grid), plane, 0, i, 3, 1);
+    }
+}
+
+/* FIXME: This doesn't work if the font has an avar table */
+static float
+denorm_coord (hb_ot_var_axis_info_t *axis, int coord)
+{
+  float r = coord / 16384.0;
+
+  if (coord < 0)
+    return axis->default_value + r * (axis->default_value - axis->min_value);
+  else
+    return axis->default_value + r * (axis->max_value - axis->default_value);
+}
+
+static void
+update_variations (void)
+{
+  GtkWidget *child;
+  Pango2Font *pango_font = NULL;
+  hb_font_t *hb_font;
+  hb_face_t *hb_face;
+  unsigned int n_axes;
+  hb_ot_var_axis_info_t *ai = NULL;
+  float *design_coords = NULL;
+  const int *coords;
+  unsigned int length;
+  int i;
+
+  while ((child = gtk_widget_get_first_child (demo->variations_grid)))
+    gtk_grid_remove (GTK_GRID (demo->variations_grid), child);
+
+  demo->instance_combo = NULL;
+
+  g_hash_table_remove_all (demo->axes);
+  g_hash_table_remove_all (demo->instances);
+
+  pango_font = get_pango_font ();
+  hb_font = pango2_font_get_hb_font (pango_font);
+  hb_face = hb_font_get_face (hb_font);
+
+  n_axes = hb_ot_var_get_axis_infos (hb_face, 0, NULL, NULL);
+  if (n_axes == 0)
+    goto done;
+
+  ai = g_new0 (hb_ot_var_axis_info_t, n_axes);
+  design_coords = g_new (float, n_axes);
+
+  hb_ot_var_get_axis_infos (hb_face, 0, &n_axes, ai);
+  coords = hb_font_get_var_coords_normalized (hb_font, &length);
+  for (i = 0; i < length; i++)
+    design_coords[i] = denorm_coord (&ai[i], coords[i]);
+
+  if (hb_ot_var_get_named_instance_count (hb_face) > 0)
+    {
+      GtkWidget *label;
+      GtkWidget *combo;
+
+      label = gtk_label_new ("Instance");
+      gtk_label_set_xalign (GTK_LABEL (label), 0);
+      gtk_widget_set_halign (label, GTK_ALIGN_START);
+      gtk_widget_set_valign (label, GTK_ALIGN_BASELINE);
+      gtk_grid_attach (GTK_GRID (demo->variations_grid), label, 0, -1, 1, 1);
+
+      combo = gtk_combo_box_text_new ();
+      gtk_widget_set_halign (combo, GTK_ALIGN_START);
+      gtk_widget_set_valign (combo, GTK_ALIGN_BASELINE);
+
+      gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), "");
+
+      for (i = 0; i < hb_ot_var_get_named_instance_count (hb_face); i++)
+        add_instance (hb_face, i, combo, i);
+
+      for (i = 0; i < hb_ot_var_get_named_instance_count (hb_face); i++)
+        {
+          if (matches_instance (hb_face, i, n_axes, design_coords))
+            {
+              gtk_combo_box_set_active (GTK_COMBO_BOX (combo), i + 1);
+              break;
+            }
+        }
+
+      gtk_grid_attach (GTK_GRID (demo->variations_grid), combo, 1, -1, 3, 1);
+      g_signal_connect (combo, "changed", G_CALLBACK (instance_changed), NULL);
+      demo->instance_combo = combo;
+   }
+
+  for (i = 0; i < n_axes; i++)
+    add_axis (hb_face, &ai[i], design_coords[i], i);
+
+  add_font_plane (n_axes);
+
+done:
+  g_clear_object (&pango_font);
+  g_free (ai);
+  g_free (design_coords);
+}
+
+static void
+palette_changed (GtkCheckButton *button)
+{
+  demo->palette = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (button), "palette"));
+  update_display ();
+}
+
+static void
+update_colors (void)
+{
+  Pango2Font *pango_font = NULL;
+  hb_font_t *hb_font;
+  hb_face_t *hb_face;
+  GtkWidget *child;
+
+  while ((child = gtk_widget_get_first_child (demo->colors_grid)))
+    gtk_grid_remove (GTK_GRID (demo->colors_grid), child);
+
+  pango_font = get_pango_font ();
+  hb_font = pango2_font_get_hb_font (pango_font);
+  hb_face = hb_font_get_face (hb_font);
+
+  if (hb_ot_color_has_palettes (hb_face))
+    {
+      demo->first_palette = NULL;
+
+      for (unsigned int i = 0; i < hb_ot_color_palette_get_count (hb_face); i++)
+        {
+          hb_ot_name_id_t name_id;
+          char *name;
+          unsigned int n_colors;
+          hb_color_t *colors;
+          GtkWidget *palette;
+          GtkWidget *swatch;
+          hb_ot_color_palette_flags_t flags;
+          const char *str;
+          GtkWidget *toggle;
+
+          name_id = hb_ot_color_palette_get_name_id (hb_face, i);
+          if (name_id != HB_OT_NAME_ID_INVALID)
+            {
+              unsigned int len;
+              char buf[80];
+
+              len = sizeof (buf);
+              hb_ot_name_get_utf8 (hb_face, name_id, HB_LANGUAGE_INVALID, &len, buf);
+              name = g_strdup (buf);
+            }
+          else
+            name = g_strdup_printf ("Palette %d", i);
+
+          toggle = gtk_check_button_new_with_label (name);
+          if (i == demo->palette)
+            gtk_check_button_set_active (GTK_CHECK_BUTTON (toggle), TRUE);
+
+          g_object_set_data (G_OBJECT (toggle), "palette", GUINT_TO_POINTER (i));
+          g_signal_connect (toggle, "toggled", G_CALLBACK (palette_changed), NULL);
+
+          if (demo->first_palette)
+            gtk_check_button_set_group (GTK_CHECK_BUTTON (toggle), GTK_CHECK_BUTTON (demo->first_palette));
+          else
+            demo->first_palette = toggle;
+
+          g_free (name);
+
+          gtk_grid_attach (GTK_GRID (demo->colors_grid), toggle, 0, i, 1, 1);
+
+          flags = hb_ot_color_palette_get_flags (hb_face, i);
+          if ((flags & (HB_OT_COLOR_PALETTE_FLAG_USABLE_WITH_LIGHT_BACKGROUND |
+                        HB_OT_COLOR_PALETTE_FLAG_USABLE_WITH_DARK_BACKGROUND)) ==
+                        (HB_OT_COLOR_PALETTE_FLAG_USABLE_WITH_LIGHT_BACKGROUND |
+                         HB_OT_COLOR_PALETTE_FLAG_USABLE_WITH_DARK_BACKGROUND))
+            str = "(light, dark)";
+          else if (flags & HB_OT_COLOR_PALETTE_FLAG_USABLE_WITH_LIGHT_BACKGROUND)
+            str = "(light)";
+          else if (flags & HB_OT_COLOR_PALETTE_FLAG_USABLE_WITH_DARK_BACKGROUND)
+            str = "(dark)";
+          else
+            str = NULL;
+          if (str)
+            gtk_grid_attach (GTK_GRID (demo->colors_grid), gtk_label_new (str), 1, i, 1, 1);
+
+          n_colors = hb_ot_color_palette_get_colors (hb_face, i, 0, NULL, NULL);
+          colors = g_new (hb_color_t, n_colors);
+          n_colors = hb_ot_color_palette_get_colors (hb_face, i, 0, &n_colors, colors);
+
+          palette = gtk_grid_new ();
+          gtk_grid_attach (GTK_GRID (demo->colors_grid), palette, 2, i, 1, 1);
+
+          for (int k = 0; k < n_colors; k++)
+            {
+              swatch = g_object_new (g_type_from_name ("GtkColorSwatch"),
+                                     "rgba", &(GdkRGBA){ hb_color_get_red (colors[k])/255.,
+                                                         hb_color_get_green (colors[k])/255.,
+                                                         hb_color_get_blue (colors[k])/255.,
+                                                         hb_color_get_alpha (colors[k])/255.},
+                                     "width-request", 16,
+                                     "height-request", 16,
+                                     NULL);
+              gtk_grid_attach (GTK_GRID (palette), swatch, k % 8, k / 8, 1, 1);
+            }
+        }
+    }
+}
+
+static void
+font_features_reset_colors (void)
+{
+  gtk_check_button_set_active (GTK_CHECK_BUTTON (demo->first_palette), TRUE);
+}
+
+static void
+font_features_font_changed (void)
+{
+  update_basic ();
+  update_script_combo ();
+  update_features ();
+  update_variations ();
+  update_colors ();
+}
+
+static void
+font_features_script_changed (void)
+{
+  update_features ();
+  update_display ();
+}
+
+static void
+font_features_reset_features (void)
+{
+  GList *l;
+
+  gtk_label_select_region (GTK_LABEL (demo->the_label), 0, 0);
+
+  g_list_free_full (demo->ranges, free_range);
+  demo->ranges = NULL;
+
+  for (l = demo->feature_items; l; l = l->next)
+    {
+      FeatureItem *item = l->data;
+
+      if (GTK_IS_CHECK_BUTTON (item->feat))
+        {
+          if (strcmp (item->name, "xxxx") == 0)
+            gtk_check_button_set_active (GTK_CHECK_BUTTON (item->feat), TRUE);
+          else
+            {
+              gtk_check_button_set_active (GTK_CHECK_BUTTON (item->feat), FALSE);
+              set_inconsistent (GTK_CHECK_BUTTON (item->feat), TRUE);
+            }
+        }
+    }
+}
+
+static void
+font_features_toggle_edit (void)
+{
+  if (strcmp (gtk_stack_get_visible_child_name (GTK_STACK (demo->stack)), "entry") != 0)
+    {
+      GtkTextBuffer *buffer;
+      GtkTextIter start, end;
+
+      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (demo->entry));
+      gtk_text_buffer_get_bounds (buffer, &start, &end);
+      g_free (demo->text);
+      demo->text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+      gtk_stack_set_visible_child_name (GTK_STACK (demo->stack), "entry");
+      gtk_widget_grab_focus (demo->entry);
+      gtk_adjustment_set_value (gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (demo->swin)), 0);
+    }
+  else
+    {
+      g_clear_pointer (&demo->text, g_free);
+      gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (demo->plain_toggle), TRUE);
+      update_display ();
+    }
+}
+
+static void
+font_features_stop_edit (void)
+{
+  g_signal_emit_by_name (demo->edit_toggle, "clicked");
+}
+
+static gboolean
+entry_key_press (GtkEventController *controller,
+                 guint               keyval,
+                 guint               keycode,
+                 GdkModifierType     modifiers,
+                 GtkTextView        *entry)
+{
+  if (keyval == GDK_KEY_Escape)
+    {
+      gtk_text_buffer_set_text (gtk_text_view_get_buffer (entry), demo->text, -1);
+      return GDK_EVENT_STOP;
+    }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static const char *paragraphs[] = {
+  "Grumpy wizards make toxic brew for the evil Queen and Jack. A quick movement of the enemy will jeopardize 
six gunboats. The job of waxing linoleum frequently peeves chintzy kids. My girl wove six dozen plaid jackets 
before she quit. Twelve ziggurats quickly jumped a finch box.",
+  "Разъяренный чтец эгоистично бьёт пятью жердями шустрого фехтовальщика. Наш банк вчера же выплатил Ф.Я. 
Эйхгольду комиссию за ценные вещи. Эх, чужак, общий съём цен шляп (юфть) – вдрызг! В чащах юга жил бы цитрус? 
Да, но фальшивый экземпляр!",
+  "Τάχιστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός",
+};
+
+static const char *alphabets[] = {
+  "abcdefghijklmnopqrstuvwxzy",
+  "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
+  "0123456789",
+  "!@#$%^&*/?;",
+};
+
+static void
+set_text_alphabet (void)
+{
+  demo->sample++;
+  gtk_text_buffer_set_text (gtk_text_view_get_buffer (GTK_TEXT_VIEW (demo->entry)),
+                            alphabets[demo->sample % G_N_ELEMENTS (alphabets)],
+                            -1);
+  update_display ();
+}
+
+static void
+set_text_paragraph (void)
+{
+  demo->sample++;
+  gtk_text_buffer_set_text (gtk_text_view_get_buffer (GTK_TEXT_VIEW (demo->entry)),
+                            paragraphs[demo->sample % G_N_ELEMENTS (paragraphs)],
+                            -1);
+  update_display ();
+}
+
+GtkWidget *
+do_font_features (GtkWidget *do_widget)
+{
+  static GtkWidget *window = NULL;
+
+  if (!window)
+    {
+      GtkBuilder *builder;
+      GtkBuilderScope *scope;
+      GtkEventController *controller;
+
+      builder = gtk_builder_new ();
+
+      scope = gtk_builder_cscope_new ();
+      gtk_builder_cscope_add_callback (scope, basic_value_changed);
+      gtk_builder_cscope_add_callback (scope, basic_entry_activated);
+      gtk_builder_cscope_add_callback (scope, color_set_cb);
+      gtk_builder_cscope_add_callback (scope, swap_colors);
+      gtk_builder_cscope_add_callback (scope, font_features_reset_basic);
+      gtk_builder_cscope_add_callback (scope, font_features_reset_features);
+      gtk_builder_cscope_add_callback (scope, font_features_reset_variations);
+      gtk_builder_cscope_add_callback (scope, font_features_reset_colors);
+      gtk_builder_cscope_add_callback (scope, font_features_toggle_plain);
+      gtk_builder_cscope_add_callback (scope, font_features_toggle_edit);
+      gtk_builder_cscope_add_callback (scope, font_features_stop_edit);
+      gtk_builder_cscope_add_callback (scope, font_features_font_changed);
+      gtk_builder_cscope_add_callback (scope, font_features_script_changed);
+      gtk_builder_cscope_add_callback (scope, font_features_notify_waterfall);
+      gtk_builder_cscope_add_callback (scope, set_text_alphabet);
+      gtk_builder_cscope_add_callback (scope, set_text_paragraph);
+      gtk_builder_set_scope (builder, scope);
+
+      demo = g_new0 (FontFeaturesDemo, 1);
+
+      gtk_builder_add_from_resource (builder, "/font_features/font_features.ui", NULL);
+
+      window = GTK_WIDGET (gtk_builder_get_object (builder, "window"));
+      g_object_set_data_full  (G_OBJECT (window), "demo", demo, demo_free);
+
+      demo->the_label = GTK_WIDGET (gtk_builder_get_object (builder, "label"));
+      demo->settings = GTK_WIDGET (gtk_builder_get_object (builder, "settings"));
+      demo->description = GTK_WIDGET (gtk_builder_get_object (builder, "description"));
+      demo->font = GTK_WIDGET (gtk_builder_get_object (builder, "font"));
+      demo->script_lang = GTK_WIDGET (gtk_builder_get_object (builder, "script_lang"));
+      demo->feature_list = GTK_WIDGET (gtk_builder_get_object (builder, "feature_list"));
+      demo->stack = GTK_WIDGET (gtk_builder_get_object (builder, "stack"));
+      demo->entry = GTK_WIDGET (gtk_builder_get_object (builder, "entry"));
+      demo->plain_toggle = GTK_WIDGET (gtk_builder_get_object (builder, "plain_toggle"));
+      demo->waterfall_toggle = GTK_WIDGET (gtk_builder_get_object (builder, "waterfall_toggle"));
+      demo->edit_toggle = GTK_WIDGET (gtk_builder_get_object (builder, "edit_toggle"));
+      demo->size_scale = GTK_WIDGET (gtk_builder_get_object (builder, "size_scale"));
+      demo->size_entry = GTK_WIDGET (gtk_builder_get_object (builder, "size_entry"));
+      demo->size_adjustment = GTK_ADJUSTMENT (gtk_builder_get_object (builder, "size_adjustment"));
+      demo->letterspacing_entry = GTK_WIDGET (gtk_builder_get_object (builder, "letterspacing_entry"));
+      demo->letterspacing_adjustment = GTK_ADJUSTMENT (gtk_builder_get_object (builder, 
"letterspacing_adjustment"));
+      demo->line_height_entry = GTK_WIDGET (gtk_builder_get_object (builder, "line_height_entry"));
+      demo->line_height_adjustment = GTK_ADJUSTMENT (gtk_builder_get_object (builder, 
"line_height_adjustment"));
+      demo->foreground = GTK_WIDGET (gtk_builder_get_object (builder, "foreground"));
+      demo->background = GTK_WIDGET (gtk_builder_get_object (builder, "background"));
+      demo->swin = GTK_WIDGET (gtk_builder_get_object (builder, "swin"));
+      demo->variations_grid = GTK_WIDGET (gtk_builder_get_object (builder, "variations_grid"));
+      demo->colors_grid = GTK_WIDGET (gtk_builder_get_object (builder, "colors_grid"));
+
+      demo->provider = gtk_css_provider_new ();
+      gtk_style_context_add_provider (gtk_widget_get_style_context (demo->swin),
+                                      GTK_STYLE_PROVIDER (demo->provider), 800);
+
+      basic_value_changed (demo->size_adjustment, demo->size_entry);
+      basic_value_changed (demo->letterspacing_adjustment, demo->letterspacing_entry);
+      basic_value_changed (demo->line_height_adjustment, demo->line_height_entry);
+
+      controller = gtk_event_controller_key_new ();
+      g_signal_connect (controller, "key-pressed", G_CALLBACK (entry_key_press), demo->entry);
+      gtk_widget_add_controller (demo->entry, controller);
+
+      add_check_group (demo->feature_list, _("Kerning"),
+                       (const char *[]){ "kern", NULL });
+      add_check_group (demo->feature_list, _("Ligatures"),
+                       (const char *[]){ "liga", "dlig", "hlig", "clig", "rlig", NULL });
+      add_check_group (demo->feature_list, _("Letter Case"),
+                       (const char *[]){ "smcp", "c2sc", "pcap", "c2pc", "unic", "cpsp",
+                                         "case",NULL });
+      add_radio_group (demo->feature_list, _("Number Case"),
+                       (const char *[]){ "xxxx", "lnum", "onum", NULL });
+      add_radio_group (demo->feature_list, _("Number Spacing"),
+                       (const char *[]){ "xxxx", "pnum", "tnum", NULL });
+      add_radio_group (demo->feature_list, _("Fractions"),
+                       (const char *[]){ "xxxx", "frac", "afrc", NULL });
+      add_check_group (demo->feature_list, _("Numeric Extras"),
+                       (const char *[]){ "zero", "nalt", "sinf", NULL });
+      add_check_group (demo->feature_list, _("Character Alternatives"),
+                       (const char *[]){ "swsh", "cswh", "locl", "calt", "falt", "hist",
+                                         "salt", "jalt", "titl", "rand", "subs", "sups",
+                                         "ordn", "ltra", "ltrm", "rtla", "rtlm", "rclt", NULL });
+      add_check_group (demo->feature_list, _("Positional Alternatives"),
+                       (const char *[]){ "init", "medi", "med2", "fina", "fin2", "fin3",
+                                         "isol", NULL });
+      add_check_group (demo->feature_list, _("Width Variants"),
+                       (const char *[]){ "fwid", "hwid", "halt", "pwid", "palt", "twid",
+                                         "qwid", NULL });
+      add_check_group (demo->feature_list, _("Alternative Stylistic Sets"),
+                       (const char *[]){ "ss01", "ss02", "ss03", "ss04", "ss05", "ss06",
+                                         "ss07", "ss08", "ss09", "ss10", "ss11", "ss12",
+                                         "ss13", "ss14", "ss15", "ss16", "ss17", "ss18",
+                                         "ss19", "ss20", NULL });
+      add_check_group (demo->feature_list, _("Character Variants"),
+                       (const char *[]){ "cv01", "cv02", "cv03", "cv04", "cv05", "cv06",
+                                         "cv07", "cv08", "cv09", "cv10", "cv11", "cv12",
+                                         "cv13", "cv14", "cv15", "cv16", "cv17", "cv18",
+                                         "cv19", "cv20", NULL });
+      add_check_group (demo->feature_list, _("Mathematical"),
+                       (const char *[]){ "dtls", "flac", "mgrk", "ssty", NULL });
+      add_check_group (demo->feature_list, _("Optical Bounds"),
+                       (const char *[]){ "opbd", "lfbd", "rtbd", NULL });
+      demo->feature_items = g_list_reverse (demo->feature_items);
+
+      if (demo->instances == NULL)
+        demo->instances = g_hash_table_new_full (instance_hash, instance_equal, NULL, free_instance);
+      else
+        g_hash_table_remove_all (demo->instances);
+
+      if (demo->axes == NULL)
+        demo->axes = g_hash_table_new_full (axes_hash, axes_equal, NULL, g_free);
+      else
+        g_hash_table_remove_all (demo->axes);
+
+      font_features_font_changed ();
+
+      g_object_add_weak_pointer (G_OBJECT (window), (gpointer *)&window);
+
+      g_object_unref (builder);
+
+      update_display ();
+    }
+
+  if (!gtk_widget_get_visible (window))
+    gtk_window_present (GTK_WINDOW (window));
+  else
+    gtk_window_destroy (GTK_WINDOW (window));
+
+  return window;
+}
diff --git a/demos/font-explorer/fontcolors.c b/demos/font-explorer/fontcolors.c
new file mode 100644
index 0000000000..26ec404524
--- /dev/null
+++ b/demos/font-explorer/fontcolors.c
@@ -0,0 +1,287 @@
+#include "fontcolors.h"
+#include "rangeedit.h"
+#include <gtk/gtk.h>
+#include <hb-ot.h>
+
+enum {
+  PROP_FONT_DESC = 1,
+  PROP_PALETTE,
+  NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
+
+struct _FontColors
+{
+  GtkWidget parent;
+
+  GtkGrid *label;
+  GtkGrid *grid;
+  Pango2FontDescription *font_desc;
+  GSimpleAction *reset_action;
+  gboolean has_colors;
+  char *palette;
+  GtkCheckButton *default_check;
+};
+
+struct _FontColorsClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE (FontColors, font_colors, GTK_TYPE_WIDGET);
+
+static Pango2Font *
+get_font (FontColors *self)
+{
+  Pango2Context *context;
+
+  context = gtk_widget_get_pango_context (GTK_WIDGET (self));
+  return pango2_context_load_font (context, self->font_desc);
+}
+
+static void
+palette_changed (GtkCheckButton *button,
+                 FontColors     *self)
+{
+  g_free (self->palette);
+  self->palette = g_strdup ((const char *) g_object_get_data (G_OBJECT (button), "palette"));
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PALETTE]);
+}
+
+static void
+update_colors (FontColors *self)
+{
+  Pango2Font *font;
+  hb_font_t *hb_font;
+  hb_face_t *hb_face;
+  GtkWidget *child;
+  GtkWidget *check;
+  unsigned int n_colors;
+  hb_color_t *colors;
+  GtkWidget *box;
+
+  g_object_ref (self->label);
+
+  while ((child = gtk_widget_get_first_child (GTK_WIDGET (self->grid))))
+    gtk_grid_remove (self->grid, child);
+
+  gtk_grid_attach (self->grid, GTK_WIDGET (self->label), 0, -4, 2, 1);
+  g_object_unref (self->label);
+
+  self->default_check = NULL;
+
+  font = get_font (self);
+  hb_font = pango2_font_get_hb_font (font);
+  hb_face = hb_font_get_face (hb_font);
+
+  self->has_colors = hb_ot_color_has_palettes (hb_face);
+  gtk_widget_set_visible (GTK_WIDGET (self), self->has_colors);
+  if (!self->has_colors)
+    {
+      g_simple_action_set_enabled (self->reset_action, FALSE);
+      return;
+    }
+
+  box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10);
+  gtk_box_set_homogeneous (GTK_BOX (box), TRUE);
+  gtk_grid_attach (self->grid, box, 0, -3, 2, 1);
+
+  check = gtk_check_button_new_with_label ("Default");
+  g_object_set_data (G_OBJECT (check), "palette", (gpointer)"default");
+  if (g_strcmp0 ("default", self->palette) == 0)
+    gtk_check_button_set_active (GTK_CHECK_BUTTON (check), TRUE);
+  g_signal_connect (check, "toggled", G_CALLBACK (palette_changed), self);
+  gtk_box_append (GTK_BOX (box), check);
+  self->default_check = GTK_CHECK_BUTTON (check);
+
+  check = gtk_check_button_new_with_label ("Light");
+  g_object_set_data (G_OBJECT (check), "palette", (gpointer)"light");
+  if (g_strcmp0 ("light", self->palette) == 0)
+    gtk_check_button_set_active (GTK_CHECK_BUTTON (check), TRUE);
+  g_signal_connect (check, "toggled", G_CALLBACK (palette_changed), self);
+  gtk_check_button_set_group (GTK_CHECK_BUTTON (check), self->default_check);
+  gtk_box_append (GTK_BOX (box), check);
+
+  check = gtk_check_button_new_with_label ("Dark");
+  g_object_set_data (G_OBJECT (check), "palette", (gpointer)"dark");
+  if (g_strcmp0 ("dark", self->palette) == 0)
+    gtk_check_button_set_active (GTK_CHECK_BUTTON (check), TRUE);
+  g_signal_connect (check, "toggled", G_CALLBACK (palette_changed), self);
+  gtk_check_button_set_group (GTK_CHECK_BUTTON (check), self->default_check);
+  gtk_box_append (GTK_BOX (box), check);
+
+  for (int i = 0; i < hb_ot_color_palette_get_count (hb_face); i++)
+    {
+      char *id = g_strdup_printf ("palette%d", i);
+      char *label = g_strdup_printf ("Palette %d", i);
+      GtkWidget *palette;
+
+      check = gtk_check_button_new_with_label (label);
+      g_object_set_data_full (G_OBJECT (check), "palette", id, g_free);
+      if (g_strcmp0 (id, self->palette) == 0)
+        gtk_check_button_set_active (GTK_CHECK_BUTTON (check), TRUE);
+      g_signal_connect (check, "toggled", G_CALLBACK (palette_changed), self);
+      gtk_check_button_set_group (GTK_CHECK_BUTTON (check), self->default_check);
+      gtk_grid_attach (self->grid, check, 0, i, 1, 1);
+
+      n_colors = hb_ot_color_palette_get_colors (hb_face, i, 0, NULL, NULL);
+      colors = g_new (hb_color_t, n_colors);
+      n_colors = hb_ot_color_palette_get_colors (hb_face, i, 0, &n_colors, colors);
+
+      palette = gtk_grid_new ();
+      gtk_widget_set_valign (palette, GTK_ALIGN_CENTER);
+      gtk_grid_attach (self->grid, palette, 1, i, 1, 1);
+
+      /* HACK - defeat first-child/last-child theming */
+      gtk_grid_attach (GTK_GRID (palette), gtk_picture_new (), -1, 0, 1, 1);
+
+      for (int k = 0; k < n_colors; k++)
+        {
+          GtkWidget *swatch;
+          swatch = g_object_new (g_type_from_name ("GtkColorSwatch"),
+                                 "rgba", &(GdkRGBA){ hb_color_get_red (colors[k])/255.,
+                                                     hb_color_get_green (colors[k])/255.,
+                                                     hb_color_get_blue (colors[k])/255.,
+                                                     hb_color_get_alpha (colors[k])/255.},
+                                 "selectable", FALSE,
+                                 "has-menu", FALSE,
+                                 "can-drag", FALSE,
+                                 "width-request", 16,
+                                 "height-request", 16,
+                                 NULL);
+          gtk_grid_attach (GTK_GRID (palette), swatch, k % 6, k / 6, 1, 1);
+        }
+
+       /* HACK - defeat first-child/last-child theming */
+       gtk_grid_attach (GTK_GRID (palette), gtk_picture_new (), 6, 0, 1, 1);
+    }
+}
+
+static void
+reset (GSimpleAction *action,
+       GVariant      *parameter,
+       FontColors   *self)
+{
+  g_free (self->palette);
+  self->palette = g_strdup (PANGO2_COLOR_PALETTE_DEFAULT);
+  if (self->has_colors)
+    gtk_check_button_set_active (self->default_check, TRUE);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PALETTE]);
+  g_simple_action_set_enabled (self->reset_action, FALSE);
+}
+
+static void
+font_colors_init (FontColors *self)
+{
+  self->palette = g_strdup (PANGO2_COLOR_PALETTE_DEFAULT);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->reset_action = g_simple_action_new ("reset", NULL);
+  g_simple_action_set_enabled (self->reset_action, FALSE);
+  g_signal_connect (self->reset_action, "activate", G_CALLBACK (reset), self);
+}
+
+static void
+font_colors_dispose (GObject *object)
+{
+  gtk_widget_clear_template (GTK_WIDGET (object), FONT_COLORS_TYPE);
+
+  G_OBJECT_CLASS (font_colors_parent_class)->dispose (object);
+}
+
+static void
+font_colors_finalize (GObject *object)
+{
+  FontColors *self = FONT_COLORS (object);
+
+  g_clear_pointer (&self->font_desc, pango2_font_description_free);
+  g_free (self->palette);
+
+  G_OBJECT_CLASS (font_colors_parent_class)->finalize (object);
+}
+
+static void
+font_colors_set_property (GObject      *object,
+                          unsigned int  prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  FontColors *self = FONT_COLORS (object);
+
+  switch (prop_id)
+    {
+    case PROP_FONT_DESC:
+      pango2_font_description_free (self->font_desc);
+      self->font_desc = pango2_font_description_copy (g_value_get_boxed (value));
+      update_colors (self);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+font_colors_get_property (GObject      *object,
+                          unsigned int  prop_id,
+                          GValue       *value,
+                          GParamSpec   *pspec)
+{
+  FontColors *self = FONT_COLORS (object);
+
+  switch (prop_id)
+    {
+    case PROP_PALETTE:
+      g_value_set_string (value, self->palette);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+font_colors_class_init (FontColorsClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = font_colors_dispose;
+  object_class->finalize = font_colors_finalize;
+  object_class->get_property = font_colors_get_property;
+  object_class->set_property = font_colors_set_property;
+
+  properties[PROP_FONT_DESC] =
+      g_param_spec_boxed ("font-desc", "", "",
+                          PANGO2_TYPE_FONT_DESCRIPTION,
+                          G_PARAM_WRITABLE);
+
+  properties[PROP_PALETTE] =
+      g_param_spec_string ("palette", "", "",
+                           PANGO2_COLOR_PALETTE_DEFAULT,
+                           G_PARAM_READABLE);
+
+  g_object_class_install_properties (G_OBJECT_CLASS (class), NUM_PROPERTIES, properties);
+
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
+                                               "/org/gtk/fontexplorer/fontcolors.ui");
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontColors, grid);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontColors, label);
+
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (class), "fontcolors");
+}
+
+FontColors *
+font_colors_new (void)
+{
+  return g_object_new (FONT_COLORS_TYPE, NULL);
+}
+
+GAction *
+font_colors_get_reset_action (FontColors *self)
+{
+  return G_ACTION (self->reset_action);
+}
diff --git a/demos/font-explorer/fontcolors.h b/demos/font-explorer/fontcolors.h
new file mode 100644
index 0000000000..d3abcf6ddc
--- /dev/null
+++ b/demos/font-explorer/fontcolors.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+
+#define FONT_COLORS_TYPE (font_colors_get_type ())
+#define FONT_COLORS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FONT_COLORS_TYPE, FontColors))
+
+
+typedef struct _FontColors         FontColors;
+typedef struct _FontColorsClass    FontColorsClass;
+
+
+GType        font_colors_get_type          (void);
+FontColors * font_colors_new               (void);
+GAction *    font_colors_get_reset_action  (FontColors *self);
diff --git a/demos/font-explorer/fontcolors.ui b/demos/font-explorer/fontcolors.ui
new file mode 100644
index 0000000000..460119c05f
--- /dev/null
+++ b/demos/font-explorer/fontcolors.ui
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FontColors" parent="GtkWidget">
+    <property name="layout-manager"><object class="GtkBinLayout"/></property>
+    <child>
+      <object class="GtkGrid" id="grid">
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="label" translatable="yes">Colors</property>
+            <property name="margin-bottom">10</property>
+            <property name="xalign">0</property>
+            <style>
+              <class name="heading"/>
+            </style>
+            <layout>
+              <property name="row">-2</property>
+              <property name="column">0</property>
+              <property name="column-span">2</property>
+            </layout>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/demos/font-explorer/fontcontrols.c b/demos/font-explorer/fontcontrols.c
new file mode 100644
index 0000000000..77c02932aa
--- /dev/null
+++ b/demos/font-explorer/fontcontrols.c
@@ -0,0 +1,271 @@
+#include "fontcontrols.h"
+#include "rangeedit.h"
+
+#include <gtk/gtk.h>
+#include <hb-ot.h>
+
+enum {
+  PROP_SIZE = 1,
+  PROP_LETTERSPACING,
+  PROP_LINE_HEIGHT,
+  PROP_FOREGROUND,
+  PROP_BACKGROUND,
+  PROP_DISABLE_SIZE,
+  NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
+
+struct _FontControls
+{
+  GtkWidget parent;
+
+  GtkAdjustment *size_adjustment;
+  GtkAdjustment *letterspacing_adjustment;
+  GtkAdjustment *line_height_adjustment;
+  GtkColorButton *foreground;
+  GtkColorButton *background;
+
+  GSimpleAction *reset_action;
+  gboolean disable_size;
+};
+
+struct _FontControlsClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE(FontControls, font_controls, GTK_TYPE_WIDGET);
+
+static void
+size_changed (GtkAdjustment *adjustment,
+              FontControls   *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SIZE]);
+  g_simple_action_set_enabled (self->reset_action, TRUE);
+}
+
+static void
+letterspacing_changed (GtkAdjustment *adjustment,
+                       FontControls   *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LETTERSPACING]);
+  g_simple_action_set_enabled (self->reset_action, TRUE);
+}
+
+static void
+line_height_changed (GtkAdjustment *adjustment,
+                     FontControls   *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_LINE_HEIGHT]);
+  g_simple_action_set_enabled (self->reset_action, TRUE);
+}
+
+static void
+color_set (GtkColorButton *button,
+           GParamSpec     *pspec,
+           FontControls    *self)
+{
+  if (button == self->foreground)
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FOREGROUND]);
+  else
+    g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_BACKGROUND]);
+  g_simple_action_set_enabled (self->reset_action, TRUE);
+}
+
+static void
+swap_colors (GtkButton   *button,
+             FontControls *self)
+{
+  GdkRGBA fg;
+  GdkRGBA bg;
+
+  gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (self->foreground), &fg);
+  gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (self->background), &bg);
+  gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (self->foreground), &bg);
+  gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (self->background), &fg);
+}
+
+static void
+reset (GSimpleAction *action,
+       GVariant      *parameter,
+       FontControls   *self)
+{
+  gtk_adjustment_set_value (self->size_adjustment, 12.);
+  gtk_adjustment_set_value (self->letterspacing_adjustment, 0.);
+  gtk_adjustment_set_value (self->line_height_adjustment, 1.);
+
+  gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (self->foreground), &(GdkRGBA){0., 0., 0., 1.0 });
+  gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (self->background), &(GdkRGBA){1., 1., 1., 1.0 });
+
+  g_simple_action_set_enabled (self->reset_action, FALSE);
+}
+
+static void
+font_controls_init (FontControls *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->reset_action = g_simple_action_new ("reset", NULL);
+  g_simple_action_set_enabled (self->reset_action, FALSE);
+  g_signal_connect (self->reset_action, "activate", G_CALLBACK (reset), self);
+}
+
+static void
+font_controls_dispose (GObject *object)
+{
+  gtk_widget_clear_template (GTK_WIDGET (object), FONT_CONTROLS_TYPE);
+
+  G_OBJECT_CLASS (font_controls_parent_class)->dispose (object);
+}
+
+static void
+font_controls_finalize (GObject *object)
+{
+  //FontControls *self = FONT_CONTROLS (object);
+
+  G_OBJECT_CLASS (font_controls_parent_class)->finalize (object);
+}
+
+static void
+font_controls_set_property (GObject      *object,
+                           unsigned int  prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  FontControls *self = FONT_CONTROLS (object);
+
+  switch (prop_id)
+    {
+    case PROP_SIZE:
+      gtk_adjustment_set_value (self->size_adjustment, g_value_get_float (value));
+      break;
+
+    case PROP_DISABLE_SIZE:
+      self->disable_size = g_value_get_boolean (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+font_controls_get_property (GObject      *object,
+                           unsigned int  prop_id,
+                           GValue       *value,
+                           GParamSpec   *pspec)
+{
+  FontControls *self = FONT_CONTROLS (object);
+
+  switch (prop_id)
+    {
+    case PROP_SIZE:
+      g_value_set_float (value, gtk_adjustment_get_value (self->size_adjustment));
+      break;
+
+    case PROP_LETTERSPACING:
+      g_value_set_int (value, (int) gtk_adjustment_get_value (self->letterspacing_adjustment));
+      break;
+
+    case PROP_LINE_HEIGHT:
+      g_value_set_float (value, gtk_adjustment_get_value (self->line_height_adjustment));
+      break;
+
+    case PROP_FOREGROUND:
+      {
+        GdkRGBA rgba;
+        gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (self->foreground), &rgba);
+        g_value_set_boxed (value, &rgba);
+      }
+      break;
+
+    case PROP_BACKGROUND:
+      {
+        GdkRGBA rgba;
+        gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (self->background), &rgba);
+        g_value_set_boxed (value, &rgba);
+      }
+      break;
+
+    case PROP_DISABLE_SIZE:
+      g_value_set_boolean (value, self->disable_size);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+font_controls_class_init (FontControlsClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  g_type_ensure (RANGE_EDIT_TYPE);
+
+  object_class->dispose = font_controls_dispose;
+  object_class->finalize = font_controls_finalize;
+  object_class->get_property = font_controls_get_property;
+  object_class->set_property = font_controls_set_property;
+
+  properties[PROP_SIZE] =
+      g_param_spec_float ("size", "", "",
+                          0., 100., 12.,
+                          G_PARAM_READABLE);
+
+  properties[PROP_LETTERSPACING] =
+      g_param_spec_int ("letterspacing", "", "",
+                        -G_MAXINT, G_MAXINT, 0,
+                        G_PARAM_READABLE);
+
+  properties[PROP_LINE_HEIGHT] =
+      g_param_spec_float ("line-height", "", "",
+                          0., 100., 1.,
+                          G_PARAM_READABLE);
+
+  properties[PROP_FOREGROUND] =
+      g_param_spec_boxed ("foreground", "", "",
+                          GDK_TYPE_RGBA,
+                          G_PARAM_READABLE);
+
+  properties[PROP_BACKGROUND] =
+      g_param_spec_boxed ("background", "", "",
+                          GDK_TYPE_RGBA,
+                          G_PARAM_READABLE);
+
+  properties[PROP_DISABLE_SIZE] =
+      g_param_spec_boolean ("disable-size", "", "",
+                            FALSE,
+                            G_PARAM_READWRITE);
+
+  g_object_class_install_properties (G_OBJECT_CLASS (class), NUM_PROPERTIES, properties);
+
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
+                                               "/org/gtk/fontexplorer/fontcontrols.ui");
+
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontControls, size_adjustment);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontControls, letterspacing_adjustment);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontControls, line_height_adjustment);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontControls, foreground);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontControls, background);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), size_changed);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), letterspacing_changed);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), line_height_changed);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), color_set);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), swap_colors);
+
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (class), "fontcontrols");
+}
+
+FontControls *
+font_controls_new (void)
+{
+  return g_object_new (FONT_CONTROLS_TYPE, NULL);
+}
+
+GAction *
+font_controls_get_reset_action (FontControls *self)
+{
+  return G_ACTION (self->reset_action);
+}
diff --git a/demos/font-explorer/fontcontrols.h b/demos/font-explorer/fontcontrols.h
new file mode 100644
index 0000000000..a15d4f408b
--- /dev/null
+++ b/demos/font-explorer/fontcontrols.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+
+#define FONT_CONTROLS_TYPE (font_controls_get_type ())
+#define FONT_CONTROLS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FONT_CONTROLS_TYPE, FontControls))
+
+
+typedef struct _FontControls         FontControls;
+typedef struct _FontControlsClass    FontControlsClass;
+
+
+GType           font_controls_get_type          (void);
+FontControls *  font_controls_new               (void);
+GAction *       font_controls_get_reset_action  (FontControls *self);
diff --git a/demos/font-explorer/fontcontrols.ui b/demos/font-explorer/fontcontrols.ui
new file mode 100644
index 0000000000..3d5d5127da
--- /dev/null
+++ b/demos/font-explorer/fontcontrols.ui
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FontControls" parent="GtkWidget">
+    <property name="layout-manager"><object class="GtkGridLayout"/></property>
+    <child>
+      <object class="GtkLabel">
+        <property name="sensitive" bind-source="FontControls" bind-property="disable-size" 
bind-flags="invert-boolean"/>
+        <property name="label">Size</property>
+        <property name="xalign">0</property>
+        <property name="valign">baseline</property>
+        <layout>
+          <property name="column">0</property>
+          <property name="row">0</property>
+        </layout>
+      </object>
+    </child>
+    <child>
+      <object class="RangeEdit">
+        <property name="sensitive" bind-source="FontControls" bind-property="disable-size" 
bind-flags="invert-boolean"/>
+        <property name="hexpand">1</property>
+        <property name="width-request">160</property>
+        <property name="valign">baseline</property>
+        <property name="adjustment">
+          <object class="GtkAdjustment" id="size_adjustment">
+            <property name="lower">7</property>
+            <property name="upper">100</property>
+            <property name="value">14</property>
+            <property name="step_increment">0.5</property>
+            <property name="page_increment">10</property>
+            <signal name="value-changed" handler="size_changed"/>
+          </object>
+        </property>
+        <property name="default-value">12</property>
+        <property name="n-chars">5</property>
+        <layout>
+          <property name="column">1</property>
+          <property name="row">0</property>
+          <property name="column-span">2</property>
+        </layout>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkLabel">
+        <property name="label">Letterspacing</property>
+        <property name="xalign">0</property>
+        <property name="valign">baseline</property>
+        <layout>
+          <property name="column">0</property>
+          <property name="row">1</property>
+        </layout>
+      </object>
+    </child>
+    <child>
+      <object class="RangeEdit">
+        <property name="hexpand">1</property>
+        <property name="width-request">160</property>
+        <property name="valign">baseline</property>
+        <property name="adjustment">
+          <object class="GtkAdjustment" id="letterspacing_adjustment">
+            <property name="lower">-1024</property>
+            <property name="upper">8192</property>
+            <property name="value">0</property>
+            <property name="step_increment">1</property>
+            <property name="page_increment">512</property>
+            <signal name="value-changed" handler="letterspacing_changed"/>
+          </object>
+        </property>
+        <property name="default-value">0</property>
+        <property name="n-chars">5</property>
+        <layout>
+          <property name="column">1</property>
+          <property name="row">1</property>
+          <property name="column-span">2</property>
+        </layout>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkLabel">
+        <property name="label">Line Height</property>
+        <property name="xalign">0</property>
+        <property name="valign">baseline</property>
+        <layout>
+          <property name="column">0</property>
+          <property name="row">2</property>
+        </layout>
+      </object>
+    </child>
+    <child>
+      <object class="RangeEdit">
+        <property name="hexpand">1</property>
+        <property name="width-request">160</property>
+        <property name="valign">baseline</property>
+        <property name="adjustment">
+          <object class="GtkAdjustment" id="line_height_adjustment">
+            <property name="lower">0.75</property>
+            <property name="upper">2.5</property>
+            <property name="value">1.0</property>
+            <property name="step_increment">0.1</property>
+            <property name="page_increment">1</property>
+            <signal name="value-changed" handler="line_height_changed"/>
+          </object>
+        </property>
+        <property name="default-value">1</property>
+        <property name="n-chars">5</property>
+        <layout>
+          <property name="column">1</property>
+          <property name="row">2</property>
+          <property name="column-span">2</property>
+        </layout>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkLabel">
+        <property name="label">Foreground</property>
+        <property name="xalign">0</property>
+        <property name="valign">baseline</property>
+        <layout>
+          <property name="column">0</property>
+          <property name="row">3</property>
+        </layout>
+      </object>
+    </child>
+    <child>
+      <object class="GtkColorButton" id="foreground">
+        <property name="rgba">black</property>
+        <signal name="notify::rgba" handler="color_set"/>
+        <layout>
+          <property name="column">1</property>
+          <property name="row">3</property>
+        </layout>
+      </object>
+    </child>
+
+    <child>
+      <object class="GtkLabel">
+        <property name="label">Background</property>
+        <property name="xalign">0</property>
+        <property name="valign">baseline</property>
+        <layout>
+          <property name="column">0</property>
+          <property name="row">4</property>
+        </layout>
+      </object>
+    </child>
+    <child>
+      <object class="GtkColorButton" id="background">
+        <property name="rgba">white</property>
+        <signal name="notify::rgba" handler="color_set"/>
+        <layout>
+          <property name="column">1</property>
+          <property name="row">4</property>
+        </layout>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton">
+        <property name="icon-name">object-flip-vertical-symbolic</property>
+        <property name="halign">start</property>
+        <property name="valign">center</property>
+        <style>
+          <class name="circular"/>
+        </style>
+        <signal name="clicked" handler="swap_colors"/>
+        <layout>
+          <property name="column">2</property>
+          <property name="row">3</property>
+          <property name="row-span">2</property>
+        </layout>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/demos/font-explorer/fontexplorer.css b/demos/font-explorer/fontexplorer.css
new file mode 100644
index 0000000000..bae9d7e768
--- /dev/null
+++ b/demos/font-explorer/fontexplorer.css
@@ -0,0 +1,20 @@
+box.sidebar {
+  padding: 20px;
+  border-spacing: 20px;
+}
+fontcontrols {
+  border-spacing: 10px;
+}
+fontvariations > grid {
+  border-spacing: 10px;
+}
+fontcolors > grid {
+  border-spacing: 10px;
+}
+samplechooser {
+  border-spacing: 10px;
+}
+fontview {
+  padding: 10px;
+  border-spacing: 10px;
+}
diff --git a/demos/font-explorer/fontexplorer.gresource.xml b/demos/font-explorer/fontexplorer.gresource.xml
new file mode 100644
index 0000000000..8715abd461
--- /dev/null
+++ b/demos/font-explorer/fontexplorer.gresource.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/org/gtk/fontexplorer">
+    <file preprocess="xml-stripblanks">fontexplorerwin.ui</file>
+    <file preprocess="xml-stripblanks">fontview.ui</file>
+    <file preprocess="xml-stripblanks">fontcontrols.ui</file>
+    <file preprocess="xml-stripblanks">samplechooser.ui</file>
+    <file preprocess="xml-stripblanks">fontcolors.ui</file>
+    <file preprocess="xml-stripblanks">fontfeatures.ui</file>
+    <file preprocess="xml-stripblanks">fontvariations.ui</file>
+    <file preprocess="xml-stripblanks">rangeedit.ui</file>
+    <file>fontexplorer.css</file>
+  </gresource>
+</gresources>
diff --git a/demos/font-explorer/fontexplorerapp.c b/demos/font-explorer/fontexplorerapp.c
new file mode 100644
index 0000000000..d98276ed17
--- /dev/null
+++ b/demos/font-explorer/fontexplorerapp.c
@@ -0,0 +1,165 @@
+#include "config.h"
+#include <gtk/gtk.h>
+
+#include "fontexplorerapp.h"
+#include "fontexplorerwin.h"
+
+#include "demo_conf.h"
+
+struct _FontExplorerApp
+{
+  GtkApplication parent;
+};
+
+struct _FontExplorerAppClass
+{
+  GtkApplicationClass parent_class;
+};
+
+G_DEFINE_TYPE(FontExplorerApp, font_explorer_app, GTK_TYPE_APPLICATION);
+
+static void
+font_explorer_app_init (FontExplorerApp *app)
+{
+}
+
+static void
+quit_activated (GSimpleAction *action,
+                GVariant      *parameter,
+                gpointer       app)
+{
+  g_application_quit (G_APPLICATION (app));
+}
+
+static void
+inspector_activated (GSimpleAction *action,
+                     GVariant      *parameter,
+                     gpointer       app)
+{
+  gtk_window_set_interactive_debugging (TRUE);
+}
+
+static void
+about_activated (GSimpleAction *action,
+                 GVariant      *parameter,
+                 gpointer       user_data)
+{
+  GtkApplication *app = user_data;
+  const char *authors[] = {
+    "The GTK Team",
+    NULL
+  };
+  char *icon_theme;
+  char *version;
+  GString *s;
+  char *os_name;
+  char *os_version;
+
+  g_object_get (gtk_settings_get_default (),
+                "gtk-icon-theme-name", &icon_theme,
+                NULL);
+
+  s = g_string_new ("");
+
+  os_name = g_get_os_info (G_OS_INFO_KEY_NAME);
+  os_version = g_get_os_info (G_OS_INFO_KEY_VERSION_ID);
+  if (os_name && os_version)
+    g_string_append_printf (s, "OS\t%s %s\n\n", os_name, os_version);
+  g_string_append (s, "System libraries\n");
+  g_string_append_printf (s, "\tGLib\t%d.%d.%d\n",
+                          glib_major_version,
+                          glib_minor_version,
+                          glib_micro_version);
+  g_string_append_printf (s, "\tPango2\t%s\n",
+                          pango2_version_string ());
+  g_string_append_printf (s, "\tGTK \t%d.%d.%d\n",
+                          gtk_get_major_version (),
+                          gtk_get_minor_version (),
+                          gtk_get_micro_version ());
+  g_string_append_printf (s, "\nIcon theme\n\t%s", icon_theme);
+  version = g_strdup_printf ("%s%s%s\nRunning against GTK %d.%d.%d",
+                             PACKAGE_VERSION,
+                             g_strcmp0 (PROFILE, "devel") == 0 ? "-" : "",
+                             g_strcmp0 (PROFILE, "devel") == 0 ? VCS_TAG : "",
+                             gtk_get_major_version (),
+                             gtk_get_minor_version (),
+                             gtk_get_micro_version ());
+
+  gtk_show_about_dialog (GTK_WINDOW (gtk_application_get_active_window (app)),
+                         "program-name", g_strcmp0 (PROFILE, "devel") == 0
+                                         ? "GTK Font Explorer (Development)"
+                                         : "GTK Font Explorer",
+                         "version", version,
+                         "copyright", "© 1997—2021 The GTK Team",
+                         "license-type", GTK_LICENSE_LGPL_2_1,
+                         "website", "http://www.gtk.org";,
+                         "comments", "Program to explore font features",
+                         "authors", authors,
+                         "logo-icon-name", "org.gtk.FontExplorer",
+                         "title", "About GTK Font Explorer",
+                         "system-information", s->str,
+                         NULL);
+
+  g_string_free (s, TRUE);
+  g_free (version);
+  g_free (icon_theme);
+  g_free (os_name);
+  g_free (os_version);
+}
+
+static GActionEntry app_entries[] =
+{
+  { "quit", quit_activated, NULL, NULL, NULL },
+  { "inspector", inspector_activated, NULL, NULL, NULL },
+  { "about", about_activated, NULL, NULL, NULL }
+};
+
+static void
+font_explorer_app_startup (GApplication *app)
+{
+  const char *quit_accels[2] = { "<Ctrl>Q", NULL };
+  GtkCssProvider *provider;
+
+  G_APPLICATION_CLASS (font_explorer_app_parent_class)->startup (app);
+
+  g_action_map_add_action_entries (G_ACTION_MAP (app),
+                                   app_entries, G_N_ELEMENTS (app_entries),
+                                   app);
+  gtk_application_set_accels_for_action (GTK_APPLICATION (app),
+                                         "app.quit",
+                                         quit_accels);
+
+  provider = gtk_css_provider_new ();
+  gtk_css_provider_load_from_resource (provider, "/org/gtk/fontexplorer/fontexplorer.css");
+  gtk_style_context_add_provider_for_display (gdk_display_get_default (),
+                                              GTK_STYLE_PROVIDER (provider),
+                                              GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+}
+
+static void
+font_explorer_app_activate (GApplication *app)
+{
+  FontExplorerWindow *win;
+
+  win = font_explorer_window_new (FONT_EXPLORER_APP (app));
+
+  if (g_strcmp0 (PROFILE, "devel") == 0)
+    gtk_widget_add_css_class (GTK_WIDGET (win), "devel");
+
+  gtk_window_present (GTK_WINDOW (win));
+}
+
+static void
+font_explorer_app_class_init (FontExplorerAppClass *class)
+{
+  G_APPLICATION_CLASS (class)->startup = font_explorer_app_startup;
+  G_APPLICATION_CLASS (class)->activate = font_explorer_app_activate;
+}
+
+FontExplorerApp *
+font_explorer_app_new (void)
+{
+  return g_object_new (FONT_EXPLORER_APP_TYPE,
+                       "application-id", "org.gtk.FontExplorer",
+                       NULL);
+}
diff --git a/demos/font-explorer/fontexplorerapp.h b/demos/font-explorer/fontexplorerapp.h
new file mode 100644
index 0000000000..d1d27defee
--- /dev/null
+++ b/demos/font-explorer/fontexplorerapp.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+
+#define FONT_EXPLORER_APP_TYPE (font_explorer_app_get_type ())
+#define FONT_EXPLORER_APP(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FONT_EXPLORER_APP_TYPE, FontExplorerApp))
+
+
+typedef struct _FontExplorerApp       FontExplorerApp;
+typedef struct _FontExplorerAppClass  FontExplorerAppClass;
+
+
+GType                   font_explorer_app_get_type    (void);
+FontExplorerApp *       font_explorer_app_new         (void);
diff --git a/demos/font-explorer/fontexplorerwin.c b/demos/font-explorer/fontexplorerwin.c
new file mode 100644
index 0000000000..0356134c7f
--- /dev/null
+++ b/demos/font-explorer/fontexplorerwin.c
@@ -0,0 +1,131 @@
+#include "fontexplorerapp.h"
+#include "fontexplorerwin.h"
+#include "fontview.h"
+#include "fontcontrols.h"
+#include "samplechooser.h"
+#include "fontcolors.h"
+#include "fontfeatures.h"
+#include "fontvariations.h"
+
+#include <gtk/gtk.h>
+#include <string.h>
+
+
+struct _FontExplorerWindow
+{
+  GtkApplicationWindow parent;
+
+  GtkFontButton *fontbutton;
+  FontControls *controls;
+  FontFeatures *features;
+  FontVariations *variations;
+  FontColors *colors;
+  FontView *view;
+};
+
+struct _FontExplorerWindowClass
+{
+  GtkApplicationWindowClass parent_class;
+};
+
+G_DEFINE_TYPE(FontExplorerWindow, font_explorer_window, GTK_TYPE_APPLICATION_WINDOW);
+
+static void
+reset (GSimpleAction      *action,
+       GVariant           *parameter,
+       FontExplorerWindow *win)
+{
+  g_action_activate (font_controls_get_reset_action (win->controls), NULL);
+  g_action_activate (font_features_get_reset_action (win->features), NULL);
+  g_action_activate (font_variations_get_reset_action (win->variations), NULL);
+  g_action_activate (font_colors_get_reset_action (win->colors), NULL);
+}
+
+static void
+update_reset (GSimpleAction      *action,
+              GParamSpec         *pspec,
+              FontExplorerWindow *win)
+{
+  gboolean enabled;
+  GAction *reset_action;
+
+  enabled = g_action_get_enabled (font_controls_get_reset_action (win->controls)) ||
+            g_action_get_enabled (font_features_get_reset_action (win->features)) ||
+            g_action_get_enabled (font_variations_get_reset_action (win->variations)) ||
+            g_action_get_enabled (font_colors_get_reset_action (win->colors));
+
+  reset_action = g_action_map_lookup_action (G_ACTION_MAP (win), "reset");
+
+  g_simple_action_set_enabled (G_SIMPLE_ACTION (reset_action), enabled);
+}
+
+static void
+font_explorer_window_init (FontExplorerWindow *win)
+{
+  GSimpleAction *reset_action;
+
+  gtk_widget_init_template (GTK_WIDGET (win));
+
+  reset_action = g_simple_action_new ("reset", NULL);
+  g_signal_connect (reset_action, "activate", G_CALLBACK (reset), win);
+  g_signal_connect (font_controls_get_reset_action (win->controls),
+                    "notify::enabled", G_CALLBACK (update_reset), win);
+  g_signal_connect (font_variations_get_reset_action (win->variations),
+                    "notify::enabled", G_CALLBACK (update_reset), win);
+  g_signal_connect (font_colors_get_reset_action (win->colors),
+                    "notify::enabled", G_CALLBACK (update_reset), win);
+  g_signal_connect (font_features_get_reset_action (win->features),
+                    "notify::enabled", G_CALLBACK (update_reset), win);
+
+  g_action_map_add_action (G_ACTION_MAP (win), G_ACTION (reset_action));
+  update_reset (NULL, NULL, win);
+}
+
+static void
+font_explorer_window_dispose (GObject *object)
+{
+  gtk_widget_clear_template (GTK_WIDGET (object), FONT_EXPLORER_WINDOW_TYPE);
+
+  G_OBJECT_CLASS (font_explorer_window_parent_class)->dispose (object);
+}
+
+static void
+font_explorer_window_finalize (GObject *object)
+{
+//  FontExplorerWindow *win = FONT_EXPLORER_WINDOW (object);
+
+  G_OBJECT_CLASS (font_explorer_window_parent_class)->finalize (object);
+}
+
+static void
+font_explorer_window_class_init (FontExplorerWindowClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  g_type_ensure (FONT_VIEW_TYPE);
+  g_type_ensure (FONT_CONTROLS_TYPE);
+  g_type_ensure (SAMPLE_CHOOSER_TYPE);
+  g_type_ensure (FONT_VARIATIONS_TYPE);
+  g_type_ensure (FONT_COLORS_TYPE);
+  g_type_ensure (FONT_FEATURES_TYPE);
+
+  object_class->dispose = font_explorer_window_dispose;
+  object_class->finalize = font_explorer_window_finalize;
+
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
+                                               "/org/gtk/fontexplorer/fontexplorerwin.ui");
+
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontExplorerWindow, fontbutton);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontExplorerWindow, controls);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontExplorerWindow, variations);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontExplorerWindow, colors);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontExplorerWindow, features);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontExplorerWindow, view);
+
+}
+
+FontExplorerWindow *
+font_explorer_window_new (FontExplorerApp *app)
+{
+  return g_object_new (FONT_EXPLORER_WINDOW_TYPE, "application", app, NULL);
+}
diff --git a/demos/font-explorer/fontexplorerwin.h b/demos/font-explorer/fontexplorerwin.h
new file mode 100644
index 0000000000..77716775b6
--- /dev/null
+++ b/demos/font-explorer/fontexplorerwin.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <gtk/gtk.h>
+#include "fontexplorerapp.h"
+
+
+#define FONT_EXPLORER_WINDOW_TYPE (font_explorer_window_get_type ())
+#define FONT_EXPLORER_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FONT_EXPLORER_WINDOW_TYPE, 
FontExplorerWindow))
+
+
+typedef struct _FontExplorerWindow         FontExplorerWindow;
+typedef struct _FontExplorerWindowClass    FontExplorerWindowClass;
+
+
+GType                   font_explorer_window_get_type     (void);
+FontExplorerWindow *    font_explorer_window_new          (FontExplorerApp    *app);
diff --git a/demos/font-explorer/fontexplorerwin.ui b/demos/font-explorer/fontexplorerwin.ui
new file mode 100644
index 0000000000..512f56541b
--- /dev/null
+++ b/demos/font-explorer/fontexplorerwin.ui
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <menu id="gear_menu">
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">_Inspector</attribute>
+        <attribute name="action">app.inspector</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">_About GTK Font Explorer</attribute>
+        <attribute name="action">app.about</attribute>
+      </item>
+    </section>
+  </menu>
+  <template class="FontExplorerWindow" parent="GtkApplicationWindow">
+    <property name="title" translatable="yes">Font Explorer</property>
+    <property name="default-width">1024</property>
+    <property name="default-height">768</property>
+    <child type="titlebar">
+      <object class="GtkHeaderBar">
+        <child>
+          <object class="GtkButton" id="reset">
+            <property name="receives-default">1</property>
+            <property name="tooltip-text">Reset</property>
+            <property name="icon-name">view-refresh-symbolic</property>
+            <property name="action-name">win.reset</property>
+          </object>
+        </child>
+        <child type="end">
+          <object class="GtkMenuButton" id="gear_menu_button">
+            <property name="focus-on-click">0</property>
+            <property name="valign">center</property>
+            <property name="menu-model">gear_menu</property>
+            <property name="icon-name">open-menu-symbolic</property>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox">
+        <child>
+          <object class="GtkScrolledWindow">
+            <property name="hscrollbar-policy">never</property>
+            <child>
+              <object class="GtkBox">
+                <property name="orientation">vertical</property>
+                <style>
+                  <class name="sidebar"/>
+                </style>
+                <child>
+                  <object class="GtkFontButton" id="fontbutton">
+                    <property name="level">family|style</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="FontControls" id="controls">
+                    <property name="disable-size" bind-source="view" bind-property="ignore-size" 
bind-flags="sync-create"/>
+                  </object>
+                </child>
+                <child>
+                  <object class="SampleChooser" id="samplechooser"/>
+                </child>
+                <child>
+                  <object class="FontVariations" id="variations">
+                    <property name="font-desc" bind-source="fontbutton" bind-flags="sync-create"/>
+                  </object>
+                </child>
+                <child>
+                  <object class="FontFeatures" id="features">
+                    <property name="font-desc" bind-source="fontbutton" bind-flags="sync-create"/>
+                    <property name="language" bind-source="fontbutton" bind-flags="sync-create"/>
+                  </object>
+                </child>
+                <child>
+                  <object class="FontColors" id="colors">
+                    <property name="font-desc" bind-source="fontbutton" bind-flags="sync-create"/>
+                  </object>
+                </child>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="FontView" id="view">
+            <property name="font-desc" bind-source="fontbutton" bind-flags="sync-create"/>
+            <property name="size" bind-source="controls" bind-flags="sync-create"/>
+            <property name="letterspacing" bind-source="controls" bind-flags="sync-create"/>
+            <property name="line-height" bind-source="controls" bind-flags="sync-create"/>
+            <property name="foreground" bind-source="controls" bind-flags="sync-create"/>
+            <property name="background" bind-source="controls" bind-flags="sync-create"/>
+            <property name="sample-text" bind-source="samplechooser" bind-flags="sync-create"/>
+            <property name="features" bind-source="features" bind-flags="sync-create"/>
+            <property name="variations" bind-source="variations" bind-flags="sync-create"/>
+            <property name="palette" bind-source="colors" bind-flags="sync-create"/>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/demos/font-explorer/fontfeatures.c b/demos/font-explorer/fontfeatures.c
new file mode 100644
index 0000000000..0412d974be
--- /dev/null
+++ b/demos/font-explorer/fontfeatures.c
@@ -0,0 +1,727 @@
+#include "fontfeatures.h"
+#include <gtk/gtk.h>
+#include <hb-ot.h>
+#include <glib/gi18n.h>
+
+#include "open-type-layout.h"
+#include "language-names.h"
+
+
+enum {
+  PROP_FONT_DESC = 1,
+  PROP_LANGUAGE,
+  PROP_FEATURES,
+  NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
+
+typedef struct {
+  unsigned int tag;
+  const char *name;
+  GtkWidget *feat;
+} FeatureItem;
+
+struct _FontFeatures
+{
+  GtkWidget parent;
+
+  GtkGrid *label;
+  GtkGrid *grid;
+  Pango2FontDescription *font_desc;
+  GSimpleAction *reset_action;
+  Pango2Language *lang;
+  GList *feature_items;
+};
+
+struct _FontFeaturesClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE(FontFeatures, font_features, GTK_TYPE_WIDGET);
+
+static Pango2Font *
+get_font (FontFeatures *self)
+{
+  Pango2Context *context;
+
+  context = gtk_widget_get_pango_context (GTK_WIDGET (self));
+  return pango2_context_load_font (context, self->font_desc);
+}
+
+static gboolean
+is_ssNN (const char *buf)
+{
+  return g_str_has_prefix (buf, "ss") && g_ascii_isdigit (buf[2]) && g_ascii_isdigit (buf[3]);
+}
+
+static gboolean
+is_cvNN (const char *buf)
+{
+  return g_str_has_prefix (buf, "cv") && g_ascii_isdigit (buf[2]) && g_ascii_isdigit (buf[3]);
+}
+
+static char *
+get_feature_display_name (unsigned int tag)
+{
+  int i;
+  static char buf[5] = { 0, };
+
+  if (tag == HB_TAG ('x', 'x', 'x', 'x'))
+    return g_strdup (_("Default"));
+
+  hb_tag_to_string (tag, buf);
+  if (is_ssNN (buf))
+    {
+      int num = (buf[2] - '0') * 10 + (buf[3] - '0');
+      return g_strdup_printf (g_dpgettext2 (NULL, "OpenType layout", "Stylistic Set %d"), num);
+    }
+  else if (is_cvNN (buf))
+    {
+      int num = (buf[2] - '0') * 10 + (buf[3] - '0');
+      return g_strdup_printf (g_dpgettext2 (NULL, "OpenType layout", "Character Variant %d"), num);
+    }
+
+  for (i = 0; i < G_N_ELEMENTS (open_type_layout_features); i++)
+    {
+      if (tag == open_type_layout_features[i].tag)
+        return g_strdup (g_dpgettext2 (NULL, "OpenType layout", open_type_layout_features[i].name));
+    }
+
+  g_warning ("unknown OpenType layout feature tag: %s", buf);
+
+  return g_strdup (buf);
+}
+
+static void
+update_feature_label (FontFeatures *self,
+                      FeatureItem  *item,
+                      hb_font_t    *hb_font,
+                      hb_tag_t      script_tag,
+                      hb_tag_t      lang_tag)
+{
+  hb_face_t *hb_face;
+  unsigned int script_index, lang_index, feature_index;
+  hb_ot_name_id_t id;
+  unsigned int len;
+  char *label;
+  char name[5] = { 0, };
+
+  hb_face = hb_font_get_face (hb_font);
+
+  hb_tag_to_string (item->tag, name);
+  if (!is_ssNN (name) && !is_cvNN (name))
+    return;
+
+  hb_ot_layout_table_find_script (hb_face, HB_OT_TAG_GSUB, script_tag, &script_index);
+
+  G_GNUC_BEGIN_IGNORE_DEPRECATIONS
+  hb_ot_layout_script_find_language (hb_face, HB_OT_TAG_GSUB, script_index, lang_tag, &lang_index);
+  G_GNUC_END_IGNORE_DEPRECATIONS
+
+  if (hb_ot_layout_language_find_feature (hb_face, HB_OT_TAG_GSUB, script_index, lang_index, item->tag, 
&feature_index) &&
+      hb_ot_layout_feature_get_name_ids (hb_face, HB_OT_TAG_GSUB, feature_index, &id, NULL, NULL, NULL, 
NULL))
+    {
+      len = hb_ot_name_get_utf8 (hb_face, id, HB_LANGUAGE_INVALID, NULL, NULL);
+      len++;
+      label = g_new (char, len);
+      hb_ot_name_get_utf8 (hb_face, id, HB_LANGUAGE_INVALID, &len, label);
+
+      gtk_check_button_set_label (GTK_CHECK_BUTTON (item->feat), label);
+
+      g_free (label);
+    }
+  else
+    {
+      label = get_feature_display_name (item->tag);
+      gtk_check_button_set_label (GTK_CHECK_BUTTON (item->feat), label);
+      g_free (label);
+    }
+}
+
+static void
+set_inconsistent (GtkCheckButton *button,
+                  gboolean        inconsistent)
+{
+  gtk_check_button_set_inconsistent (GTK_CHECK_BUTTON (button), inconsistent);
+  gtk_widget_set_opacity (gtk_widget_get_first_child (GTK_WIDGET (button)), inconsistent ? 0.0 : 1.0);
+}
+
+static void
+find_language_and_script (FontFeatures *self,
+                          hb_face_t    *hb_face,
+                          hb_tag_t     *lang_tag,
+                          hb_tag_t     *script_tag)
+{
+  int i, j, k;
+  hb_tag_t scripts[80];
+  unsigned int n_scripts;
+  unsigned int count;
+  hb_tag_t table[2] = { HB_OT_TAG_GSUB, HB_OT_TAG_GPOS };
+  hb_language_t lang;
+  const char *langname, *p;
+
+  langname = pango2_language_to_string (self->lang);
+
+  p = strchr (langname, '-');
+  lang = hb_language_from_string (langname, p ? p - langname : -1);
+
+  n_scripts = 0;
+  for (i = 0; i < 2; i++)
+    {
+      count = G_N_ELEMENTS (scripts);
+      hb_ot_layout_table_get_script_tags (hb_face, table[i], n_scripts, &count, scripts);
+      n_scripts += count;
+    }
+
+  for (j = 0; j < n_scripts; j++)
+    {
+      hb_tag_t languages[80];
+      unsigned int n_languages;
+
+      n_languages = 0;
+      for (i = 0; i < 2; i++)
+        {
+          count = G_N_ELEMENTS (languages);
+          hb_ot_layout_script_get_language_tags (hb_face, table[i], j, n_languages, &count, languages);
+          n_languages += count;
+        }
+
+      for (k = 0; k < n_languages; k++)
+        {
+          if (lang == hb_ot_tag_to_language (languages[k]))
+            {
+              *script_tag = scripts[j];
+              *lang_tag = languages[k];
+              return;
+            }
+        }
+    }
+
+  *lang_tag = HB_OT_TAG_DEFAULT_LANGUAGE;
+  *script_tag = HB_OT_TAG_DEFAULT_SCRIPT;
+}
+
+static void
+hide_feature_maybe (FeatureItem *item,
+                    gboolean     has_feature)
+{
+  gtk_widget_set_visible (item->feat, has_feature);
+  if (has_feature)
+    gtk_widget_set_visible (gtk_widget_get_parent (item->feat), TRUE);
+}
+
+/* Make features insensitive if the font/langsys does not have them,
+ * and reset all others to their initial value
+ */
+static void
+update_features (FontFeatures *self)
+{
+  guint script_index, lang_index;
+  hb_tag_t lang_tag;
+  hb_tag_t script_tag;
+  Pango2Font *font;
+  hb_font_t *hb_font;
+  hb_face_t *hb_face;
+
+  font = get_font (self);
+  hb_font = pango2_font_get_hb_font (font);
+  hb_face = hb_font_get_face (hb_font);
+
+    {
+      hb_tag_t table[2] = { HB_OT_TAG_GSUB, HB_OT_TAG_GPOS };
+      hb_tag_t features[256];
+      unsigned int count;
+      unsigned int n_features = 0;
+
+      find_language_and_script (self, hb_face, &lang_tag, &script_tag);
+
+      /* Collect all features */
+      for (int i = 0; i < 2; i++)
+        {
+          hb_ot_layout_table_find_script (hb_face,
+                                          table[i],
+                                          script_tag,
+                                          &script_index);
+          hb_ot_layout_script_select_language (hb_face,
+                                               table[i],
+                                               script_index,
+                                               1,
+                                               &lang_tag,
+                                               &lang_index);
+
+          count = G_N_ELEMENTS (features);
+          hb_ot_layout_language_get_feature_tags (hb_face,
+                                                  table[i],
+                                                  script_index,
+                                                  lang_index,
+                                                  n_features,
+                                                  &count,
+                                                  features);
+          n_features += count;
+        }
+
+      /* Update all the features */
+      for (GList *l = self->feature_items; l; l = l->next)
+        {
+          FeatureItem *item = l->data;
+          gboolean has_feature = FALSE;
+
+          for (int j = 0; j < n_features; j++)
+            {
+              if (item->tag == features[j])
+                {
+                  has_feature = TRUE;
+                  break;
+                }
+            }
+
+          update_feature_label (self, item, hb_font, script_tag, lang_tag);
+
+          hide_feature_maybe (item, has_feature);
+
+          if (GTK_IS_CHECK_BUTTON (item->feat))
+            {
+              GtkWidget *def = GTK_WIDGET (g_object_get_data (G_OBJECT (item->feat), "default"));
+              if (def)
+                {
+                  gtk_widget_show (def);
+                  gtk_widget_show (gtk_widget_get_parent (def));
+                  gtk_check_button_set_active (GTK_CHECK_BUTTON (def), TRUE);
+                }
+              else
+                set_inconsistent (GTK_CHECK_BUTTON (item->feat), TRUE);
+            }
+        }
+    }
+
+  /* Hide empty groups */
+  for (GList *l = self->feature_items; l; l = l->next)
+    {
+      FeatureItem *item = l->data;
+      GtkWidget *box;
+
+      box = gtk_widget_get_parent (item->feat);
+      if (gtk_widget_get_visible (box))
+        {
+          GtkWidget *c;
+          int count;
+
+          count = 0;
+          for (c = gtk_widget_get_first_child (box); c; c = gtk_widget_get_next_sibling (c))
+            {
+              if (gtk_widget_get_visible (c))
+                count++;
+            }
+
+          if (count == 1)
+            gtk_widget_hide (box);
+          else if (count == 2 &&
+                   item->tag == HB_TAG ('x', 'x', 'x', 'x'))
+            gtk_widget_hide (box);
+        }
+    }
+}
+
+static void
+script_changed (GtkComboBox  *combo,
+                FontFeatures *self)
+{
+  update_features (self);
+}
+
+static char *
+get_features (FontFeatures *self)
+{
+  GString *s;
+  char buf[128];
+
+  s = g_string_new ("");
+
+  for (GList *l = self->feature_items; l; l = l->next)
+    {
+      FeatureItem *item = l->data;
+
+      if (!gtk_widget_is_sensitive (item->feat))
+        continue;
+
+      if (GTK_IS_CHECK_BUTTON (item->feat) && g_object_get_data (G_OBJECT (item->feat), "default"))
+        {
+          if (gtk_check_button_get_active (GTK_CHECK_BUTTON (item->feat)) &&
+              item->tag != HB_TAG ('x', 'x', 'x', 'x'))
+            {
+              hb_feature_to_string (&(hb_feature_t) { item->tag, 1, 0, -1 }, buf, sizeof (buf));
+              if (s->len > 0)
+                g_string_append_c (s, ',');
+              g_string_append (s, buf);
+            }
+        }
+      else if (GTK_IS_CHECK_BUTTON (item->feat))
+        {
+          guint32 value;
+
+          if (gtk_check_button_get_inconsistent (GTK_CHECK_BUTTON (item->feat)))
+            continue;
+
+          value = gtk_check_button_get_active (GTK_CHECK_BUTTON (item->feat));
+          hb_feature_to_string (&(hb_feature_t) { item->tag, value, 0, -1 }, buf, sizeof (buf));
+          if (s->len > 0)
+            g_string_append_c (s, ',');
+          g_string_append (s, buf);
+        }
+    }
+
+  return g_string_free (s, FALSE);
+}
+
+static void
+update_display (FontFeatures *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FEATURES]);
+  g_simple_action_set_enabled (self->reset_action, TRUE);
+}
+
+static GtkWidget *
+make_title_label (const char *title)
+{
+  GtkWidget *label;
+
+  label = gtk_label_new (title);
+  gtk_label_set_xalign (GTK_LABEL (label), 0.0);
+  gtk_widget_set_halign (label, GTK_ALIGN_START);
+  g_object_set (label, "margin-top", 10, "margin-bottom", 10, NULL);
+  gtk_widget_add_css_class (label, "heading");
+
+  return label;
+}
+
+static void
+feat_toggled_cb (GtkCheckButton *check_button,
+                 gpointer        data)
+{
+  set_inconsistent (check_button, FALSE);
+}
+
+static void
+feat_pressed (GtkGestureClick *gesture,
+              int              n_press,
+              double           x,
+              double           y,
+              GtkWidget       *feat)
+{
+  const guint button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+
+  if (button == GDK_BUTTON_PRIMARY)
+    {
+      g_signal_handlers_block_by_func (feat, feat_pressed, NULL);
+
+      if (gtk_check_button_get_inconsistent (GTK_CHECK_BUTTON (feat)))
+        {
+          set_inconsistent (GTK_CHECK_BUTTON (feat), FALSE);
+          gtk_check_button_set_active (GTK_CHECK_BUTTON (feat), TRUE);
+        }
+
+      g_signal_handlers_unblock_by_func (feat, feat_pressed, NULL);
+    }
+  else if (button == GDK_BUTTON_SECONDARY)
+    {
+      gboolean inconsistent = gtk_check_button_get_inconsistent (GTK_CHECK_BUTTON (feat));
+      set_inconsistent (GTK_CHECK_BUTTON (feat), !inconsistent);
+    }
+}
+
+static void
+add_check_group (FontFeatures  *self,
+                 const char    *title,
+                 const char   **tags,
+                 unsigned int   n_tags,
+                 int            row)
+{
+  GtkWidget *group;
+  int i;
+
+  group = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+  gtk_widget_set_halign (group, GTK_ALIGN_START);
+
+  gtk_box_append (GTK_BOX (group), make_title_label (title));
+
+  for (i = 0; i < n_tags; i++)
+    {
+      unsigned int tag;
+      GtkWidget *feat;
+      FeatureItem *item;
+      GtkGesture *gesture;
+      char *name;
+
+      tag = hb_tag_from_string (tags[i], -1);
+
+      name = get_feature_display_name (tag);
+      feat = gtk_check_button_new_with_label (name);
+      g_free (name);
+      set_inconsistent (GTK_CHECK_BUTTON (feat), TRUE);
+      g_signal_connect_swapped (feat, "notify::active", G_CALLBACK (update_display), self);
+      g_signal_connect_swapped (feat, "notify::inconsistent", G_CALLBACK (update_display), self);
+      g_signal_connect (feat, "toggled", G_CALLBACK (feat_toggled_cb), NULL);
+
+      gesture = gtk_gesture_click_new ();
+      gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (gesture), GDK_BUTTON_SECONDARY);
+      g_signal_connect (gesture, "pressed", G_CALLBACK (feat_pressed), feat);
+      gtk_widget_add_controller (feat, GTK_EVENT_CONTROLLER (gesture));
+
+      gtk_box_append (GTK_BOX (group), feat);
+
+      item = g_new (FeatureItem, 1);
+      item->name = tags[i];
+      item->tag = tag;
+      item->feat = feat;
+
+      self->feature_items = g_list_prepend (self->feature_items, item);
+    }
+
+  gtk_grid_attach (self->grid, group, 0, row, 2, 1);
+}
+
+static void
+add_radio_group (FontFeatures  *self,
+                 const char    *title,
+                 const char   **tags,
+                 unsigned int   n_tags,
+                 int            row)
+{
+  GtkWidget *group;
+  int i;
+  GtkWidget *group_button = NULL;
+
+  group = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+  gtk_widget_set_halign (group, GTK_ALIGN_START);
+
+  gtk_box_append (GTK_BOX (group), make_title_label (title));
+
+  for (i = 0; i < n_tags; i++)
+    {
+      unsigned int tag;
+      GtkWidget *feat;
+      FeatureItem *item;
+      char *name;
+
+      tag = hb_tag_from_string (tags[i], -1);
+      name = get_feature_display_name (tag);
+      feat = gtk_check_button_new_with_label (name ? name : _("Default"));
+      g_free (name);
+      if (group_button == NULL)
+        group_button = feat;
+      else
+        gtk_check_button_set_group (GTK_CHECK_BUTTON (feat), GTK_CHECK_BUTTON (group_button));
+      g_signal_connect_swapped (feat, "notify::active", G_CALLBACK (update_display), self);
+      g_object_set_data (G_OBJECT (feat), "default", group_button);
+
+      gtk_box_append (GTK_BOX (group), feat);
+
+      item = g_new (FeatureItem, 1);
+      item->name = tags[i];
+      item->tag = tag;
+      item->feat = feat;
+
+      self->feature_items = g_list_prepend (self->feature_items, item);
+    }
+
+  gtk_grid_attach (self->grid, group, 0, row, 2, 1);
+}
+
+static void
+setup_features (FontFeatures *self)
+{
+  const char *kerning[] = { "kern" };
+  const char *ligatures[] = { "liga", "dlig", "hlig", "clig", "rlig" };
+  const char *letter_case[] = {
+    "smcp", "c2sc", "pcap", "c2pc", "unic", "cpsp", "case"
+  };
+  const char *number_case[] = { "xxxx", "lnum", "onum" };
+  const char *number_spacing[] = { "xxxx", "pnum", "tnum" };
+  const char *fractions[] = { "xxxx", "frac", "afrc" };
+  const char *num_extras[] = { "zero", "nalt", "sinf" };
+  const char *char_alt[] = {
+    "swsh", "cswh", "locl", "calt", "falt", "hist",
+    "salt", "jalt", "titl", "rand", "subs", "sups",
+    "ordn", "ltra", "ltrm", "rtla", "rtlm", "rclt"
+  };
+  const char *pos_alt[] = {
+    "init", "medi", "med2", "fina", "fin2", "fin3", "isol"
+  };
+  const char *width_var[] = {
+    "fwid", "hwid", "halt", "pwid", "palt", "twid", "qwid"
+  };
+  const char *style_alt[] = {
+    "ss01", "ss02", "ss03", "ss04", "ss05", "ss06",
+    "ss07", "ss08", "ss09", "ss10", "ss11", "ss12",
+    "ss13", "ss14", "ss15", "ss16", "ss17", "ss18",
+    "ss19", "ss20"
+  };
+  const char *char_var[] = {
+    "cv01", "cv02", "cv03", "cv04", "cv05", "cv06",
+    "cv07", "cv08", "cv09", "cv10", "cv11", "cv12",
+    "cv13", "cv14", "cv15", "cv16", "cv17", "cv18",
+    "cv19", "cv20"
+  };
+  const char *math[] = { "dtls", "flac", "mgrk", "ssty" };
+  const char *bounds[] = { "opbd", "lfbd", "rtbd" };
+  int row = 0;
+
+  add_check_group (self, _("Kerning"), kerning, G_N_ELEMENTS (kerning), row++);
+  add_check_group (self, _("Ligatures"), ligatures, G_N_ELEMENTS (ligatures), row++);
+  add_check_group (self, _("Letter Case"), letter_case, G_N_ELEMENTS (letter_case), row++);
+  add_radio_group (self, _("Number Case"), number_case, G_N_ELEMENTS (number_case), row++);
+  add_radio_group (self, _("Number Spacing"), number_spacing, G_N_ELEMENTS (number_spacing), row++);
+  add_radio_group (self, _("Fractions"), fractions, G_N_ELEMENTS (fractions), row++);
+  add_check_group (self, _("Numeric Extras"), num_extras, G_N_ELEMENTS (num_extras), row++);
+  add_check_group (self, _("Character Alternatives"), char_alt, G_N_ELEMENTS (char_alt), row++);
+  add_check_group (self, _("Positional Alternatives"), pos_alt, G_N_ELEMENTS (pos_alt), row++);
+  add_check_group (self, _("Width Variants"), width_var, G_N_ELEMENTS (width_var), row++);
+  add_check_group (self, _("Alternative Stylistic Sets"), style_alt, G_N_ELEMENTS (style_alt), row++);
+  add_check_group (self, _("Character Variants"), char_var, G_N_ELEMENTS (char_var), row++);
+  add_check_group (self, _("Mathematical"), math, G_N_ELEMENTS (math), row++);
+  add_check_group (self, _("Optical Bounds"), bounds, G_N_ELEMENTS (bounds), row++);
+
+  self->feature_items = g_list_reverse (self->feature_items);
+}
+
+static void
+reset (GSimpleAction *action,
+       GVariant      *parameter,
+       FontFeatures   *self)
+{
+  update_features (self);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_FEATURES]);
+  g_simple_action_set_enabled (self->reset_action, FALSE);
+}
+
+static void
+font_features_init (FontFeatures *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->font_desc = pango2_font_description_from_string ("sans 12");
+  self->lang = pango2_language_get_default ();
+
+  setup_features (self);
+
+  self->reset_action = g_simple_action_new ("reset", NULL);
+  g_simple_action_set_enabled (self->reset_action, FALSE);
+  g_signal_connect (self->reset_action, "activate", G_CALLBACK (reset), self);
+}
+
+static void
+font_features_dispose (GObject *object)
+{
+  FontFeatures *self = FONT_FEATURES (object);
+
+  gtk_widget_clear_template (GTK_WIDGET (object), FONT_FEATURES_TYPE);
+
+  g_list_free_full (self->feature_items, g_free);
+
+  G_OBJECT_CLASS (font_features_parent_class)->dispose (object);
+}
+
+static void
+font_features_finalize (GObject *object)
+{
+  FontFeatures *self = FONT_FEATURES (object);
+
+  g_clear_pointer (&self->font_desc, pango2_font_description_free);
+
+  G_OBJECT_CLASS (font_features_parent_class)->finalize (object);
+}
+
+static void
+font_features_set_property (GObject      *object,
+                            unsigned int  prop_id,
+                            const GValue *value,
+                            GParamSpec   *pspec)
+{
+  FontFeatures *self = FONT_FEATURES (object);
+
+  switch (prop_id)
+    {
+    case PROP_FONT_DESC:
+      pango2_font_description_free (self->font_desc);
+      self->font_desc = pango2_font_description_copy (g_value_get_boxed (value));
+      update_features (self);
+      break;
+
+    case PROP_LANGUAGE:
+      self->lang = pango2_language_from_string (g_value_get_string (value));
+      update_features (self);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+font_features_get_property (GObject      *object,
+                              unsigned int  prop_id,
+                              GValue       *value,
+                              GParamSpec   *pspec)
+{
+  FontFeatures *self = FONT_FEATURES (object);
+
+  switch (prop_id)
+    {
+    case PROP_FEATURES:
+      g_value_take_string (value, get_features (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+font_features_class_init (FontFeaturesClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = font_features_dispose;
+  object_class->finalize = font_features_finalize;
+  object_class->get_property = font_features_get_property;
+  object_class->set_property = font_features_set_property;
+
+  properties[PROP_FONT_DESC] =
+      g_param_spec_boxed ("font-desc", "", "",
+                          PANGO2_TYPE_FONT_DESCRIPTION,
+                          G_PARAM_WRITABLE);
+
+  properties[PROP_LANGUAGE] =
+      g_param_spec_string ("language", "", "",
+                           "en",
+                           G_PARAM_WRITABLE);
+
+  properties[PROP_FEATURES] =
+      g_param_spec_string ("features", "", "",
+                           "",
+                           G_PARAM_READABLE);
+
+  g_object_class_install_properties (G_OBJECT_CLASS (class), NUM_PROPERTIES, properties);
+
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
+                                               "/org/gtk/fontexplorer/fontfeatures.ui");
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontFeatures, grid);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontFeatures, label);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), script_changed);
+
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (class), "fontfeatures");
+}
+
+FontFeatures *
+font_features_new (void)
+{
+  return g_object_new (FONT_FEATURES_TYPE, NULL);
+}
+
+GAction *
+font_features_get_reset_action (FontFeatures *self)
+{
+  return G_ACTION (self->reset_action);
+}
diff --git a/demos/font-explorer/fontfeatures.h b/demos/font-explorer/fontfeatures.h
new file mode 100644
index 0000000000..af6e92171a
--- /dev/null
+++ b/demos/font-explorer/fontfeatures.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+
+#define FONT_FEATURES_TYPE (font_features_get_type ())
+#define FONT_FEATURES(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FONT_FEATURES_TYPE, FontFeatures))
+
+
+typedef struct _FontFeatures         FontFeatures;
+typedef struct _FontFeaturesClass    FontFeaturesClass;
+
+
+GType          font_features_get_type          (void);
+FontFeatures * font_features_new               (void);
+GAction *      font_features_get_reset_action  (FontFeatures *self);
diff --git a/demos/font-explorer/fontfeatures.ui b/demos/font-explorer/fontfeatures.ui
new file mode 100644
index 0000000000..3a5ee420df
--- /dev/null
+++ b/demos/font-explorer/fontfeatures.ui
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FontFeatures" parent="GtkWidget">
+    <property name="layout-manager"><object class="GtkBinLayout"/></property>
+    <child>
+      <object class="GtkGrid" id="grid">
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="label" translatable="yes">Features</property>
+            <property name="margin-bottom">10</property>
+            <property name="xalign">0</property>
+            <style>
+              <class name="heading"/>
+            </style>
+            <layout>
+              <property name="row">-1</property>
+              <property name="column">0</property>
+              <property name="column-span">2</property>
+            </layout>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/demos/font-explorer/fontvariations.c b/demos/font-explorer/fontvariations.c
new file mode 100644
index 0000000000..e00fc3301f
--- /dev/null
+++ b/demos/font-explorer/fontvariations.c
@@ -0,0 +1,488 @@
+#include "fontvariations.h"
+#include "rangeedit.h"
+#include <gtk/gtk.h>
+#include <hb-ot.h>
+
+enum {
+  PROP_FONT_DESC = 1,
+  PROP_VARIATIONS,
+  NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
+
+struct _FontVariations
+{
+  GtkWidget parent;
+
+  GtkGrid *label;
+  GtkGrid *grid;
+  Pango2FontDescription *font_desc;
+  GSimpleAction *reset_action;
+  gboolean has_variations;
+
+  GtkWidget *instance_combo;
+  GHashTable *axes;
+  GHashTable *instances;
+};
+
+struct _FontVariationsClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE(FontVariations, font_variations, GTK_TYPE_WIDGET);
+
+static Pango2Font *
+get_font (FontVariations *self)
+{
+  Pango2Context *context;
+
+  context = gtk_widget_get_pango_context (GTK_WIDGET (self));
+  return pango2_context_load_font (context, self->font_desc);
+}
+
+typedef struct {
+  guint32 tag;
+  GtkAdjustment *adjustment;
+  double default_value;
+} Axis;
+
+static guint
+axes_hash (gconstpointer v)
+{
+  const Axis *p = v;
+
+  return p->tag;
+}
+
+static gboolean
+axes_equal (gconstpointer v1, gconstpointer v2)
+{
+  const Axis *p1 = v1;
+  const Axis *p2 = v2;
+
+  return p1->tag == p2->tag;
+}
+
+static void
+unset_instance (GtkAdjustment  *adjustment,
+                FontVariations *self)
+{
+  if (self->instance_combo)
+    gtk_combo_box_set_active (GTK_COMBO_BOX (self->instance_combo), 0);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VARIATIONS]);
+  g_simple_action_set_enabled (self->reset_action, TRUE);
+}
+
+static void
+add_axis (FontVariations        *self,
+          hb_face_t             *hb_face,
+          hb_ot_var_axis_info_t *ax,
+          int                    i)
+{
+  GtkWidget *axis_label;
+  GtkWidget *axis_scale;
+  GtkAdjustment *adjustment;
+  Axis *axis;
+  char name[20];
+  unsigned int name_len = 20;
+
+  hb_ot_name_get_utf8 (hb_face, ax->name_id, HB_LANGUAGE_INVALID, &name_len, name);
+
+  axis_label = gtk_label_new (name);
+  gtk_widget_set_halign (axis_label, GTK_ALIGN_START);
+  gtk_widget_set_valign (axis_label, GTK_ALIGN_BASELINE);
+  gtk_grid_attach (self->grid, axis_label, 0, i, 1, 1);
+  adjustment = gtk_adjustment_new (ax->default_value, ax->min_value, ax->max_value,
+                                   1.0, 10.0, 0.0);
+  axis_scale = g_object_new (RANGE_EDIT_TYPE,
+                             "adjustment", adjustment,
+                             "default-value", ax->default_value,
+                             "n-chars", 5,
+                             "hexpand", TRUE,
+                             "halign", GTK_ALIGN_FILL,
+                             "valign", GTK_ALIGN_BASELINE,
+                             NULL);
+  gtk_grid_attach (self->grid, axis_scale, 1, i, 1, 1);
+
+  axis = g_new0 (Axis, 1);
+  axis->tag = ax->tag;
+  axis->adjustment = adjustment;
+  axis->default_value = ax->default_value;
+  g_hash_table_add (self->axes, axis);
+
+  g_signal_connect (adjustment, "value-changed", G_CALLBACK (unset_instance), self);
+}
+
+typedef struct {
+  char *name;
+  unsigned int index;
+} Instance;
+
+static guint
+instance_hash (gconstpointer v)
+{
+  const Instance *p = v;
+
+  return g_str_hash (p->name);
+}
+
+static gboolean
+instance_equal (gconstpointer v1, gconstpointer v2)
+{
+  const Instance *p1 = v1;
+  const Instance *p2 = v2;
+
+  return g_str_equal (p1->name, p2->name);
+}
+
+static void
+free_instance (gpointer data)
+{
+  Instance *instance = data;
+
+  g_free (instance->name);
+  g_free (instance);
+}
+
+static void
+add_instance (FontVariations *self,
+              hb_face_t      *face,
+              unsigned int    index,
+              GtkWidget      *combo,
+              int             pos)
+{
+  Instance *instance;
+  hb_ot_name_id_t name_id;
+  char name[20];
+  unsigned int name_len = 20;
+
+  instance = g_new0 (Instance, 1);
+
+  name_id = hb_ot_var_named_instance_get_subfamily_name_id (face, index);
+  hb_ot_name_get_utf8 (face, name_id, HB_LANGUAGE_INVALID, &name_len, name);
+
+  instance->name = g_strdup (name);
+  instance->index = index;
+
+  g_hash_table_add (self->instances, instance);
+  gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), instance->name);
+}
+
+static void
+instance_changed (GtkComboBox    *combo,
+                  FontVariations *self)
+{
+  char *text;
+  Instance *instance;
+  Instance ikey;
+  int i;
+  unsigned int coords_length;
+  float *coords = NULL;
+  hb_ot_var_axis_info_t *ai = NULL;
+  unsigned int n_axes;
+  Pango2Font *font = NULL;
+  hb_font_t *hb_font;
+  hb_face_t *hb_face;
+
+  text = gtk_combo_box_text_get_active_text (GTK_COMBO_BOX_TEXT (combo));
+  if (text[0] == '\0')
+    goto out;
+
+  ikey.name = text;
+  instance = g_hash_table_lookup (self->instances, &ikey);
+  if (!instance)
+    {
+      g_print ("did not find instance %s\n", text);
+      goto out;
+    }
+
+  font = get_font (self);
+  hb_font = pango2_font_get_hb_font (font);
+  hb_face = hb_font_get_face (hb_font);
+
+  n_axes = hb_ot_var_get_axis_infos (hb_face, 0, NULL, NULL);
+  ai = g_new (hb_ot_var_axis_info_t, n_axes);
+  hb_ot_var_get_axis_infos (hb_face, 0, &n_axes, ai);
+
+  coords = g_new (float, n_axes);
+  hb_ot_var_named_instance_get_design_coords (hb_face,
+                                              instance->index,
+                                              &coords_length,
+                                              coords);
+
+  for (i = 0; i < n_axes; i++)
+    {
+      Axis *axis;
+      Axis akey;
+      double value;
+
+      value = coords[ai[i].axis_index];
+
+      akey.tag = ai[i].tag;
+      axis = g_hash_table_lookup (self->axes, &akey);
+      if (axis)
+        {
+          g_signal_handlers_block_by_func (axis->adjustment, unset_instance, self);
+          gtk_adjustment_set_value (axis->adjustment, value);
+          g_signal_handlers_unblock_by_func (axis->adjustment, unset_instance, self);
+        }
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VARIATIONS]);
+  g_simple_action_set_enabled (self->reset_action, TRUE);
+
+out:
+  g_free (text);
+  g_clear_object (&font);
+  g_free (ai);
+  g_free (coords);
+}
+
+static void
+update_variations (FontVariations *self)
+{
+  GtkWidget *child;
+  Pango2Font *font;
+  hb_font_t *hb_font;
+  hb_face_t *hb_face;
+  unsigned int n_axes;
+  hb_ot_var_axis_info_t *ai = NULL;
+  int i;
+
+  font = get_font (self);
+  hb_font = pango2_font_get_hb_font (font);
+  hb_face = hb_font_get_face (hb_font);
+
+  g_object_ref (self->label);
+
+  while ((child = gtk_widget_get_first_child (GTK_WIDGET (self->grid))))
+    gtk_grid_remove (self->grid, child);
+
+  gtk_grid_attach (self->grid, GTK_WIDGET (self->label), 0, -2, 2, 1);
+  g_object_unref (self->label);
+
+  self->instance_combo = NULL;
+  g_hash_table_remove_all (self->axes);
+  g_hash_table_remove_all (self->instances);
+
+  n_axes = hb_ot_var_get_axis_infos (hb_face, 0, NULL, NULL);
+  self->has_variations = n_axes > 0;
+  gtk_widget_set_visible (GTK_WIDGET (self), self->has_variations);
+  if (!self->has_variations)
+    {
+      g_simple_action_set_enabled (self->reset_action, FALSE);
+      return;
+    }
+
+  if (hb_ot_var_get_named_instance_count (hb_face) > 0)
+    {
+      GtkWidget *label;
+      GtkWidget *combo;
+
+      label = gtk_label_new ("Instance");
+      gtk_label_set_xalign (GTK_LABEL (label), 0);
+      gtk_widget_set_halign (label, GTK_ALIGN_START);
+      gtk_widget_set_valign (label, GTK_ALIGN_BASELINE);
+      gtk_grid_attach (self->grid, label, 0, -1, 1, 1);
+
+      combo = gtk_combo_box_text_new ();
+      gtk_widget_set_halign (combo, GTK_ALIGN_START);
+      gtk_widget_set_valign (combo, GTK_ALIGN_BASELINE);
+      gtk_widget_set_hexpand (combo, TRUE);
+
+      gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), "");
+
+      for (i = 0; i < hb_ot_var_get_named_instance_count (hb_face); i++)
+        add_instance (self, hb_face, i, combo, i);
+
+      gtk_grid_attach (GTK_GRID (self->grid), combo, 1, -1, 1, 1);
+      g_signal_connect (combo, "changed", G_CALLBACK (instance_changed), self);
+      self->instance_combo = combo;
+   }
+
+  ai = g_new (hb_ot_var_axis_info_t, n_axes);
+  hb_ot_var_get_axis_infos (hb_face, 0, &n_axes, ai);
+  for (i = 0; i < n_axes; i++)
+    add_axis (self, hb_face, &ai[i], i);
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VARIATIONS]);
+
+  g_clear_object (&font);
+  g_free (ai);
+}
+
+static char *
+get_variations (FontVariations *self)
+{
+  GHashTableIter iter;
+  Axis *axis;
+  char buf[G_ASCII_DTOSTR_BUF_SIZE];
+  const char *sep = "";
+  GString *s;
+
+  if (!self->has_variations)
+    return g_strdup ("");
+
+  s = g_string_new ("");
+
+  g_hash_table_iter_init (&iter, self->axes);
+  while (g_hash_table_iter_next (&iter, (gpointer *)NULL, (gpointer *)&axis))
+    {
+      char tag[5];
+      double value;
+
+      hb_tag_to_string (axis->tag, tag);
+      tag[4] = '\0';
+      value = gtk_adjustment_get_value (axis->adjustment);
+
+      g_string_append_printf (s, "%s%s=%s", sep, tag, g_ascii_dtostr (buf, sizeof (buf), value));
+      sep = ",";
+    }
+
+  return g_string_free (s, FALSE);
+}
+
+static void
+reset (GSimpleAction *action,
+       GVariant      *parameter,
+       FontVariations   *self)
+{
+  GHashTableIter iter;
+  Axis *axis;
+
+  if (self->instance_combo)
+    gtk_combo_box_set_active (GTK_COMBO_BOX (self->instance_combo), 0);
+
+  g_hash_table_iter_init (&iter, self->axes);
+  while (g_hash_table_iter_next (&iter, (gpointer *)NULL, (gpointer *)&axis))
+    {
+      g_signal_handlers_block_by_func (axis->adjustment, unset_instance, self);
+      gtk_adjustment_set_value (axis->adjustment, axis->default_value);
+      g_signal_handlers_unblock_by_func (axis->adjustment, unset_instance, self);
+    }
+
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_VARIATIONS]);
+  g_simple_action_set_enabled (self->reset_action, FALSE);
+}
+
+static void
+font_variations_init (FontVariations *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->reset_action = g_simple_action_new ("reset", NULL);
+  g_simple_action_set_enabled (self->reset_action, FALSE);
+  g_signal_connect (self->reset_action, "activate", G_CALLBACK (reset), self);
+
+  self->instances = g_hash_table_new_full (instance_hash, instance_equal,
+                                           NULL, free_instance);
+  self->axes = g_hash_table_new_full (axes_hash, axes_equal,
+                                      NULL, g_free);
+}
+
+static void
+font_variations_dispose (GObject *object)
+{
+  FontVariations *self = FONT_VARIATIONS (object);
+
+  gtk_widget_clear_template (GTK_WIDGET (object), FONT_VARIATIONS_TYPE);
+
+  g_hash_table_unref (self->instances);
+  g_hash_table_unref (self->axes);
+
+  G_OBJECT_CLASS (font_variations_parent_class)->dispose (object);
+}
+
+static void
+font_variations_finalize (GObject *object)
+{
+  FontVariations *self = FONT_VARIATIONS (object);
+
+  g_clear_pointer (&self->font_desc, pango2_font_description_free);
+
+  G_OBJECT_CLASS (font_variations_parent_class)->finalize (object);
+}
+
+static void
+font_variations_set_property (GObject      *object,
+                              unsigned int  prop_id,
+                              const GValue *value,
+                              GParamSpec   *pspec)
+{
+  FontVariations *self = FONT_VARIATIONS (object);
+
+  switch (prop_id)
+    {
+    case PROP_FONT_DESC:
+      pango2_font_description_free (self->font_desc);
+      self->font_desc = pango2_font_description_copy (g_value_get_boxed (value));
+      update_variations (self);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+font_variations_get_property (GObject      *object,
+                              unsigned int  prop_id,
+                              GValue       *value,
+                              GParamSpec   *pspec)
+{
+  FontVariations *self = FONT_VARIATIONS (object);
+
+  switch (prop_id)
+    {
+    case PROP_VARIATIONS:
+      g_value_take_string (value, get_variations (self));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+font_variations_class_init (FontVariationsClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = font_variations_dispose;
+  object_class->finalize = font_variations_finalize;
+  object_class->get_property = font_variations_get_property;
+  object_class->set_property = font_variations_set_property;
+
+  properties[PROP_FONT_DESC] =
+      g_param_spec_boxed ("font-desc", "", "",
+                          PANGO2_TYPE_FONT_DESCRIPTION,
+                          G_PARAM_WRITABLE);
+
+  properties[PROP_VARIATIONS] =
+      g_param_spec_string ("variations", "", "",
+                           "",
+                           G_PARAM_READABLE);
+
+  g_object_class_install_properties (G_OBJECT_CLASS (class), NUM_PROPERTIES, properties);
+
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
+                                               "/org/gtk/fontexplorer/fontvariations.ui");
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontVariations, grid);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontVariations, label);
+
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (class), "fontvariations");
+}
+
+FontVariations *
+font_variations_new (void)
+{
+  return g_object_new (FONT_VARIATIONS_TYPE, NULL);
+}
+
+GAction *
+font_variations_get_reset_action (FontVariations *self)
+{
+  return G_ACTION (self->reset_action);
+}
diff --git a/demos/font-explorer/fontvariations.h b/demos/font-explorer/fontvariations.h
new file mode 100644
index 0000000000..afd12dbaf9
--- /dev/null
+++ b/demos/font-explorer/fontvariations.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+
+#define FONT_VARIATIONS_TYPE (font_variations_get_type ())
+#define FONT_VARIATIONS(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FONT_VARIATIONS_TYPE, FontVariations))
+
+
+typedef struct _FontVariations         FontVariations;
+typedef struct _FontVariationsClass    FontVariationsClass;
+
+
+GType            font_variations_get_type          (void);
+FontVariations * font_variations_new               (void);
+GAction *        font_variations_get_reset_action  (FontVariations *self);
diff --git a/demos/font-explorer/fontvariations.ui b/demos/font-explorer/fontvariations.ui
new file mode 100644
index 0000000000..f420ac9414
--- /dev/null
+++ b/demos/font-explorer/fontvariations.ui
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FontVariations" parent="GtkWidget">
+    <property name="layout-manager"><object class="GtkBinLayout"/></property>
+    <child>
+      <object class="GtkGrid" id="grid">
+        <child>
+          <object class="GtkLabel" id="label">
+            <property name="label" translatable="yes">Variations</property>
+            <property name="margin-bottom">10</property>
+            <property name="xalign">0</property>
+            <style>
+              <class name="heading"/>
+            </style>
+            <layout>
+              <property name="row">-2</property>
+              <property name="column">0</property>
+              <property name="column-span">2</property>
+            </layout>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/demos/font-explorer/fontview.c b/demos/font-explorer/fontview.c
new file mode 100644
index 0000000000..678de65220
--- /dev/null
+++ b/demos/font-explorer/fontview.c
@@ -0,0 +1,413 @@
+#include "fontview.h"
+#include <gtk/gtk.h>
+
+enum {
+  PROP_FONT_DESC = 1,
+  PROP_SIZE,
+  PROP_LETTERSPACING,
+  PROP_LINE_HEIGHT,
+  PROP_FOREGROUND,
+  PROP_BACKGROUND,
+  PROP_VARIATIONS,
+  PROP_FEATURES,
+  PROP_PALETTE,
+  PROP_SAMPLE_TEXT,
+  PROP_IGNORE_SIZE,
+  NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
+
+struct _FontView
+{
+  GtkWidget parent;
+
+  GtkStack *stack;
+  GtkTextView *edit;
+  GtkLabel *content;
+  GtkScrolledWindow *swin;
+
+  Pango2FontDescription *font_desc;
+  float size;
+  char *variations;
+  char *features;
+  char *palette;
+  int letterspacing;
+  float line_height;
+  GdkRGBA foreground;
+  GdkRGBA background;
+  GtkCssProvider *bg_provider;
+  char *sample_text;
+  gboolean do_waterfall;
+};
+
+struct _FontViewClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE(FontView, font_view, GTK_TYPE_WIDGET);
+
+static void
+font_view_init (FontView *self)
+{
+  self->font_desc = pango2_font_description_from_string ("sans 12");
+  self->size = 12.;
+  self->letterspacing = 0;
+  self->line_height = 1.;
+  self->variations = g_strdup ("");
+  self->features = g_strdup ("");
+  self->palette = g_strdup (PANGO2_COLOR_PALETTE_DEFAULT);
+  self->foreground = (GdkRGBA){0., 0., 0., 1. };
+  self->background = (GdkRGBA){1., 1., 1., 1. };
+  self->sample_text = g_strdup ("Some sample text is better than other sample text");
+
+  gtk_widget_set_layout_manager (GTK_WIDGET (self),
+                                 gtk_box_layout_new (GTK_ORIENTATION_VERTICAL));
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  self->bg_provider = gtk_css_provider_new ();
+  gtk_style_context_add_provider (gtk_widget_get_style_context (GTK_WIDGET (self->content)),
+                                  GTK_STYLE_PROVIDER (self->bg_provider), 800);
+}
+
+static void
+font_view_dispose (GObject *object)
+{
+  gtk_widget_clear_template (GTK_WIDGET (object), FONT_VIEW_TYPE);
+
+  G_OBJECT_CLASS (font_view_parent_class)->dispose (object);
+}
+
+static void
+font_view_finalize (GObject *object)
+{
+  FontView *self = FONT_VIEW (object);
+
+  pango2_font_description_free (self->font_desc);
+  g_free (self->variations);
+  g_free (self->features);
+  g_free (self->palette);
+
+  G_OBJECT_CLASS (font_view_parent_class)->finalize (object);
+}
+
+static void
+update_view (FontView *self)
+{
+  Pango2FontDescription *desc;
+  Pango2AttrList *attrs;
+  char *fg, *bg, *css;
+
+  desc = pango2_font_description_copy_static (self->font_desc);
+  pango2_font_description_set_size (desc, 12 * PANGO2_SCALE);
+  pango2_font_description_set_variations (desc, self->variations);
+
+  attrs = pango2_attr_list_new ();
+  pango2_attr_list_insert (attrs, pango2_attr_font_desc_new (desc));
+  pango2_attr_list_insert (attrs, pango2_attr_size_new (self->size * PANGO2_SCALE));
+  pango2_attr_list_insert (attrs, pango2_attr_letter_spacing_new (self->letterspacing));
+  pango2_attr_list_insert (attrs, pango2_attr_line_height_new (self->line_height));
+  pango2_attr_list_insert (attrs, pango2_attr_foreground_new (&(Pango2Color){65535 * self->foreground.red,
+                                                                             65535 * self->foreground.green,
+                                                                             65535 * self->foreground.blue,
+                                                                             65535 * 
self->foreground.alpha}));
+  pango2_attr_list_insert (attrs, pango2_attr_font_features_new (self->features));
+  pango2_attr_list_insert (attrs, pango2_attr_palette_new (self->palette));
+
+  pango2_font_description_free (desc);
+
+  gtk_scrolled_window_set_policy (self->swin,
+                                  self->do_waterfall ? GTK_POLICY_AUTOMATIC : GTK_POLICY_NEVER,
+                                  GTK_POLICY_AUTOMATIC);
+  gtk_label_set_wrap (self->content, !self->do_waterfall);
+
+  if (self->do_waterfall)
+    {
+      GString *str;
+      int sizes[] = { 7, 8, 9, 10, 12, 14, 16, 20, 24, 30, 40, 50, 60, 70, 90 };
+      int start, text_len;
+
+      str = g_string_new ("");
+      start = 0;
+      text_len = strlen (self->sample_text);
+      for (int i = 0; i < G_N_ELEMENTS (sizes); i++)
+        {
+          Pango2Attribute *attr;
+
+          g_string_append (str, self->sample_text);
+          g_string_append (str, "
"); /* Unicode line separator */
+
+          attr = pango2_attr_size_new (sizes[i] * PANGO2_SCALE);
+          pango2_attribute_set_range (attr, start, start + text_len);
+          pango2_attr_list_insert (attrs, attr);
+          start += text_len + strlen ("
");
+        }
+      gtk_label_set_text (self->content, str->str);
+      gtk_label_set_attributes (self->content, attrs);
+      g_string_free (str, TRUE);
+    }
+  else
+    {
+      gtk_label_set_label (self->content, self->sample_text);
+      gtk_label_set_attributes (self->content, attrs);
+    }
+
+  pango2_attr_list_unref (attrs);
+
+  fg = gdk_rgba_to_string (&self->foreground);
+  bg = gdk_rgba_to_string (&self->background);
+  css = g_strdup_printf (".view_background { caret-color: %s; background-color: %s; }", fg, bg);
+  gtk_css_provider_load_from_data (self->bg_provider, css, strlen (css));
+  g_free (css);
+  g_free (fg);
+  g_free (bg);
+}
+
+static void
+toggle_edit (GtkToggleButton *button,
+             FontView        *self)
+{
+  GtkTextBuffer *buffer;
+
+  buffer = gtk_text_view_get_buffer (self->edit);
+
+  if (gtk_toggle_button_get_active (button))
+    {
+      gtk_text_buffer_set_text (buffer, self->sample_text, -1);
+      gtk_stack_set_visible_child_name (self->stack, "edit");
+      gtk_widget_grab_focus (GTK_WIDGET (self->edit));
+    }
+  else
+    {
+      GtkTextIter start, end;
+
+      g_free (self->sample_text);
+      gtk_text_buffer_get_bounds (buffer, &start, &end);
+      self->sample_text = gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
+
+      update_view (self);
+
+      gtk_stack_set_visible_child_name (self->stack, "content");
+
+      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SAMPLE_TEXT]);
+    }
+}
+
+static void
+waterfall_changed (GtkToggleButton *button,
+                   GParamSpec      *pspec,
+                   FontView        *self)
+{
+  self->do_waterfall = gtk_toggle_button_get_active (button);
+  update_view (self);
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_IGNORE_SIZE]);
+}
+
+static void
+font_view_set_property (GObject      *object,
+                        guint         prop_id,
+                        const GValue *value,
+                        GParamSpec   *pspec)
+{
+  FontView *self = FONT_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_FONT_DESC:
+      pango2_font_description_free (self->font_desc);
+      self->font_desc = pango2_font_description_copy (g_value_get_boxed (value));
+      break;
+
+    case PROP_SIZE:
+      self->size = g_value_get_float (value);
+      break;
+
+    case PROP_LETTERSPACING:
+      self->letterspacing = g_value_get_int (value);
+      break;
+
+    case PROP_LINE_HEIGHT:
+      self->line_height = g_value_get_float (value);
+      break;
+
+    case PROP_FOREGROUND:
+      self->foreground = *(GdkRGBA *)g_value_get_boxed (value);
+      break;
+
+    case PROP_BACKGROUND:
+      self->background = *(GdkRGBA *)g_value_get_boxed (value);
+      break;
+
+    case PROP_VARIATIONS:
+      g_free (self->variations);
+      self->variations = g_strdup (g_value_get_string (value));
+      break;
+
+    case PROP_FEATURES:
+      g_free (self->features);
+      self->features = g_strdup (g_value_get_string (value));
+      break;
+
+    case PROP_PALETTE:
+      g_free (self->palette);
+      self->palette = g_strdup (g_value_get_string (value));
+      break;
+
+    case PROP_SAMPLE_TEXT:
+      g_free (self->sample_text);
+      self->sample_text = g_strdup (g_value_get_string (value));
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+
+  update_view (self);
+}
+
+static void
+font_view_get_property (GObject    *object,
+                        guint       prop_id,
+                        GValue     *value,
+                        GParamSpec *pspec)
+{
+  FontView *self = FONT_VIEW (object);
+
+  switch (prop_id)
+    {
+    case PROP_FONT_DESC:
+      g_value_set_boxed (value, self->font_desc);
+      break;
+
+    case PROP_SIZE:
+      g_value_set_float (value, self->size);
+      break;
+
+    case PROP_LETTERSPACING:
+      g_value_set_int (value, self->letterspacing);
+      break;
+
+    case PROP_LINE_HEIGHT:
+      g_value_set_float (value, self->line_height);
+      break;
+
+    case PROP_FOREGROUND:
+      g_value_set_boxed (value, &self->foreground);
+      break;
+
+    case PROP_BACKGROUND:
+      g_value_set_boxed (value, &self->background);
+      break;
+
+    case PROP_VARIATIONS:
+      g_value_set_string (value, self->variations);
+      break;
+
+    case PROP_FEATURES:
+      g_value_set_string (value, self->features);
+      break;
+
+    case PROP_PALETTE:
+      g_value_set_string (value, self->palette);
+      break;
+
+    case PROP_SAMPLE_TEXT:
+      g_value_set_string (value, self->sample_text);
+      break;
+
+    case PROP_IGNORE_SIZE:
+      g_value_set_boolean (value, self->do_waterfall);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+font_view_class_init (FontViewClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = font_view_dispose;
+  object_class->finalize = font_view_finalize;
+  object_class->get_property = font_view_get_property;
+  object_class->set_property = font_view_set_property;
+
+  properties[PROP_FONT_DESC] =
+      g_param_spec_boxed ("font-desc", "", "",
+                          PANGO2_TYPE_FONT_DESCRIPTION,
+                          G_PARAM_READWRITE);
+
+  properties[PROP_SIZE] =
+      g_param_spec_float ("size", "", "",
+                          0., 100., 12.,
+                          G_PARAM_READWRITE);
+
+  properties[PROP_LETTERSPACING] =
+      g_param_spec_int ("letterspacing", "", "",
+                        -G_MAXINT, G_MAXINT, 0,
+                        G_PARAM_READWRITE);
+
+  properties[PROP_LINE_HEIGHT] =
+      g_param_spec_float ("line-height", "", "",
+                          0., 100., 1.,
+                          G_PARAM_READWRITE);
+
+  properties[PROP_FOREGROUND] =
+      g_param_spec_boxed ("foreground", "", "",
+                          GDK_TYPE_RGBA,
+                          G_PARAM_READWRITE);
+
+  properties[PROP_BACKGROUND] =
+      g_param_spec_boxed ("background", "", "",
+                          GDK_TYPE_RGBA,
+                          G_PARAM_READWRITE);
+
+  properties[PROP_VARIATIONS] =
+      g_param_spec_string ("variations", "", "",
+                           "",
+                           G_PARAM_READWRITE);
+
+  properties[PROP_FEATURES] =
+      g_param_spec_string ("features", "", "",
+                           "",
+                           G_PARAM_READWRITE);
+
+  properties[PROP_PALETTE] =
+      g_param_spec_string ("palette", "", "",
+                           PANGO2_COLOR_PALETTE_DEFAULT,
+                           G_PARAM_READWRITE);
+
+  properties[PROP_SAMPLE_TEXT] =
+      g_param_spec_string ("sample-text", "", "",
+                           "",
+                           G_PARAM_READWRITE);
+
+  properties[PROP_IGNORE_SIZE] =
+      g_param_spec_boolean ("ignore-size", "", "",
+                            FALSE,
+                            G_PARAM_READWRITE);
+
+
+  g_object_class_install_properties (G_OBJECT_CLASS (class), NUM_PROPERTIES, properties);
+
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
+                                               "/org/gtk/fontexplorer/fontview.ui");
+
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontView, swin);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontView, content);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontView, stack);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), FontView, edit);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), toggle_edit);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), waterfall_changed);
+
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (class), "fontview");
+}
+
+FontView *
+font_view_new (void)
+{
+  return g_object_new (FONT_VIEW_TYPE, NULL);
+}
diff --git a/demos/font-explorer/fontview.h b/demos/font-explorer/fontview.h
new file mode 100644
index 0000000000..fb35b524cb
--- /dev/null
+++ b/demos/font-explorer/fontview.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+
+#define FONT_VIEW_TYPE (font_view_get_type ())
+#define FONT_VIEW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), FONT_VIEW_TYPE, FontView))
+
+
+typedef struct _FontView         FontView;
+typedef struct _FontViewClass    FontViewClass;
+
+
+GType           font_view_get_type              (void);
+FontView *      font_view_new                   (void);
diff --git a/demos/font-explorer/fontview.ui b/demos/font-explorer/fontview.ui
new file mode 100644
index 0000000000..74a4b3fa23
--- /dev/null
+++ b/demos/font-explorer/fontview.ui
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="FontView" parent="GtkWidget">
+    <property name="hexpand">1</property>
+    <property name="vexpand">1</property>
+    <style>
+      <class name="view"/>
+    </style>
+    <child>
+      <object class="GtkStack" id="stack">
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">content</property>
+            <property name="child">
+              <object class="GtkScrolledWindow" id="swin">
+                <property name="hscrollbar-policy">never</property>
+                <property name="vscrollbar-policy">automatic</property>
+                <child>
+                  <object class="GtkLabel" id="content">
+                    <property name="label">Content
Content</property>
+                    <property name="wrap">1</property>
+                    <property name="wrap-mode">word-char</property>
+                    <property name="xalign">0</property>
+                    <property name="yalign">0</property>
+                    <property name="hexpand">1</property>
+                    <property name="vexpand">1</property>
+                    <property name="halign">fill</property>
+                    <property name="valign">fill</property>
+                    <style>
+                      <class name="view_background"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkStackPage">
+            <property name="name">edit</property>
+            <property name="child">
+              <object class="GtkScrolledWindow">
+                <property name="hscrollbar-policy">never</property>
+                <property name="vscrollbar-policy">automatic</property>
+                <child>
+                  <object class="GtkTextView" id="edit">
+                    <property name="wrap-mode">word-char</property>
+                  </object>
+                </child>
+              </object>
+            </property>
+          </object>
+        </child>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox">
+        <child>
+          <object class="GtkBox">
+            <style>
+              <class name="linked"/>
+            </style>
+            <child>
+              <object class="GtkToggleButton" id="plain_toggle">
+                <property name="label" translatable="yes">Plain</property>
+                <property name="active">1</property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkToggleButton" id="waterfall_toggle">
+                <property name="label" translatable="yes">Waterfall</property>
+                <property name="group">plain_toggle</property>
+                <signal name="notify::active" handler="waterfall_changed"/>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkToggleButton">
+            <property name="icon-name">document-edit-symbolic</property>
+            <property name="tooltip-text" translatable="yes">Edit the sample</property>
+            <property name="halign">end</property>
+            <property name="valign">end</property>
+            <property name="hexpand">1</property>
+            <signal name="clicked" handler="toggle_edit"/>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/demos/font-explorer/language-names.c b/demos/font-explorer/language-names.c
new file mode 100644
index 0000000000..235d87bd30
--- /dev/null
+++ b/demos/font-explorer/language-names.c
@@ -0,0 +1,336 @@
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+
+#ifdef HAVE_UNISTD_H
+#include <unistd.h>
+#endif
+
+#include <string.h>
+#include <errno.h>
+#include <locale.h>
+#include <sys/stat.h>
+
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+#include <hb-ot.h>
+
+#include "language-names.h"
+
+#ifdef G_OS_WIN32
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+#else
+#ifndef ISO_CODES_PREFIX
+#define ISO_CODES_PREFIX "/usr"
+#endif
+
+#define ISO_CODES_DATADIR ISO_CODES_PREFIX "/share/xml/iso-codes"
+#define ISO_CODES_LOCALESDIR ISO_CODES_PREFIX "/share/locale"
+#endif
+
+static GHashTable *language_map;
+
+#ifdef G_OS_WIN32
+/* if we are using native Windows use native Windows API for language names */
+static BOOL CALLBACK
+get_win32_all_locales_scripts (LPWSTR locale_w, DWORD flags, LPARAM param)
+{
+  wchar_t *langname_w = NULL;
+  wchar_t locale_abbrev_w[9];
+  gchar *langname, *locale_abbrev, *locale, *p;
+  gint i;
+  const LCTYPE iso639_lctypes[] = { LOCALE_SISO639LANGNAME, LOCALE_SISO639LANGNAME2 };
+  GHashTable *ht_scripts_langs = (GHashTable *) param;
+  Pango2Language *lang;
+
+  gint langname_size, locale_abbrev_size;
+  langname_size = GetLocaleInfoEx (locale_w, LOCALE_SLOCALIZEDDISPLAYNAME, langname_w, 0);
+  if (langname_size == 0)
+    return FALSE;
+
+  langname_w = g_new0 (wchar_t, langname_size);
+
+  if (langname_size == 0)
+    return FALSE;
+
+  GetLocaleInfoEx (locale_w, LOCALE_SLOCALIZEDDISPLAYNAME, langname_w, langname_size);
+  langname = g_utf16_to_utf8 (langname_w, -1, NULL, NULL, NULL);
+  locale = g_utf16_to_utf8 (locale_w, -1, NULL, NULL, NULL);
+  p = strchr (locale, '-');
+  lang = pango2_language_from_string (locale);
+  if (g_hash_table_lookup (ht_scripts_langs, lang) == NULL)
+    g_hash_table_insert (ht_scripts_langs, lang, langname);
+
+  /*
+   * Track 3+-letter ISO639-2/3 language codes as well (these have a max length of 9 including terminating 
NUL)
+   * ISO639-2: iso639_lctypes[0] = LOCALE_SISO639LANGNAME
+   * ISO639-3: iso639_lctypes[1] = LOCALE_SISO639LANGNAME2
+   */
+  for (i = 0; i < 2; i++)
+    {
+      locale_abbrev_size = GetLocaleInfoEx (locale_w, iso639_lctypes[i], locale_abbrev_w, 0);
+      if (locale_abbrev_size > 0)
+        {
+          GetLocaleInfoEx (locale_w, iso639_lctypes[i], locale_abbrev_w, locale_abbrev_size);
+
+          locale_abbrev = g_utf16_to_utf8 (locale_abbrev_w, -1, NULL, NULL, NULL);
+          lang = pango2_language_from_string (locale_abbrev);
+          if (g_hash_table_lookup (ht_scripts_langs, lang) == NULL)
+            g_hash_table_insert (ht_scripts_langs, lang, langname);
+
+          g_free (locale_abbrev);
+        }
+    }
+
+  g_free (locale);
+  g_free (langname_w);
+
+  return TRUE;
+}
+
+#else /* non-Windows */
+
+static char *
+get_first_item_in_semicolon_list (const char *list)
+{
+  char **items;
+  char  *item;
+
+  items = g_strsplit (list, "; ", 2);
+
+  item = g_strdup (items[0]);
+  g_strfreev (items);
+
+  return item;
+}
+
+static char *
+capitalize_utf8_string (const char *str)
+{
+  char first[8] = { 0 };
+
+  if (!str)
+    return NULL;
+
+  g_unichar_to_utf8 (g_unichar_totitle (g_utf8_get_char (str)), first);
+
+  return g_strconcat (first, g_utf8_offset_to_pointer (str, 1), NULL);
+}
+
+static char *
+get_display_name (const char *language)
+{
+  const char  *translated;
+  char *tmp;
+  char *name;
+
+  translated = dgettext ("iso_639", language);
+
+  tmp = get_first_item_in_semicolon_list (translated);
+  name = capitalize_utf8_string (tmp);
+  g_free (tmp);
+
+  return name;
+}
+
+static void
+languages_parse_start_tag (GMarkupParseContext  *ctx,
+                           const char           *element_name,
+                           const char          **attr_names,
+                           const char          **attr_values,
+                           gpointer              user_data,
+                           GError              **error)
+{
+  const char *ccode_longB;
+  const char *ccode_longT;
+  const char *ccode;
+  const char *ccode_id;
+  const char *lang_name;
+  char *display_name;
+  const char *long_names[] = {
+    "Dogri",
+    "Greek, Modern",
+    "Interlingua",
+    "Konkani",
+    "Tonga",
+    "Turkish, Ottoman",
+  };
+  int i;
+
+  if (!(g_str_equal (element_name, "iso_639_entry") ||
+        g_str_equal (element_name, "iso_639_3_entry")) ||
+        attr_names == NULL ||
+        attr_values == NULL)
+    return;
+
+  ccode = NULL;
+  ccode_longB = NULL;
+  ccode_longT = NULL;
+  ccode_id = NULL;
+  lang_name = NULL;
+
+  while (*attr_names && *attr_values)
+    {
+      if (g_str_equal (*attr_names, "iso_639_1_code"))
+        {
+          if (**attr_values)
+            {
+              if (strlen (*attr_values) != 2)
+                return;
+              ccode = *attr_values;
+            }
+        }
+      else if (g_str_equal (*attr_names, "iso_639_2B_code"))
+        {
+          if (**attr_values)
+            {
+              if (strlen (*attr_values) != 3)
+                return;
+              ccode_longB = *attr_values;
+            }
+        }
+      else if (g_str_equal (*attr_names, "iso_639_2T_code"))
+        {
+          if (**attr_values)
+            {
+              if (strlen (*attr_values) != 3)
+                return;
+              ccode_longT = *attr_values;
+            }
+        }
+      else if (g_str_equal (*attr_names, "id"))
+        {
+          if (**attr_values)
+            {
+              if (strlen (*attr_values) != 2 &&
+                  strlen (*attr_values) != 3)
+                return;
+              ccode_id = *attr_values;
+            }
+        }
+      else if (g_str_equal (*attr_names, "name"))
+        {
+          lang_name = *attr_values;
+        }
+
+      ++attr_names;
+      ++attr_values;
+    }
+
+  if (lang_name == NULL)
+    return;
+
+  display_name = get_display_name (lang_name);
+
+  /* Fix up some egregious names */
+  for (i = 0; i < G_N_ELEMENTS (long_names); i++)
+    {
+      if (g_str_has_prefix (display_name, long_names[i]))
+        display_name[strlen (long_names[i])] = '\0';
+    }
+
+
+  if (ccode != NULL)
+    g_hash_table_insert (language_map,
+                         pango2_language_from_string (ccode),
+                         g_strdup (display_name));
+
+  if (ccode_longB != NULL)
+    g_hash_table_insert (language_map,
+                         pango2_language_from_string (ccode_longB),
+                         g_strdup (display_name));
+
+  if (ccode_longT != NULL)
+    g_hash_table_insert (language_map,
+                         pango2_language_from_string (ccode_longT),
+                         g_strdup (display_name));
+
+  if (ccode_id != NULL)
+    g_hash_table_insert (language_map,
+                         pango2_language_from_string (ccode_id),
+                         g_strdup (display_name));
+
+  g_free (display_name);
+}
+
+static void
+languages_variant_init (const char *variant)
+{
+  gboolean res;
+  gsize    buf_len;
+  char *buf;
+  char *filename;
+  GError *error;
+
+  bindtextdomain (variant, ISO_CODES_LOCALESDIR);
+  bind_textdomain_codeset (variant, "UTF-8");
+
+  error = NULL;
+  filename = g_strconcat (ISO_CODES_DATADIR, "/", variant, ".xml", NULL);
+  res = g_file_get_contents (filename, &buf, &buf_len, &error);
+  if (res)
+    {
+      GMarkupParseContext *ctx = NULL;
+      GMarkupParser parser = { languages_parse_start_tag, NULL, NULL, NULL, NULL };
+
+      ctx = g_markup_parse_context_new (&parser, 0, NULL, NULL);
+
+      res = g_markup_parse_context_parse (ctx, buf, buf_len, &error);
+      g_free (ctx);
+
+      if (!res)
+        {
+          g_warning ("Failed to parse '%s': %s\n", filename, error->message);
+          g_error_free (error);
+        }
+    }
+  else
+    {
+      g_warning ("Failed to load '%s': %s\n", filename, error->message);
+      g_error_free (error);
+    }
+
+  g_free (filename);
+  g_free (buf);
+}
+
+#endif
+
+static void
+languages_init (void)
+{
+  if (language_map)
+    return;
+
+  language_map = g_hash_table_new_full (NULL, NULL, NULL, g_free);
+
+#ifdef G_OS_WIN32
+  g_return_if_fail (EnumSystemLocalesEx (&get_win32_all_locales_scripts, LOCALE_ALL, (LPARAM) language_map, 
NULL));
+#else
+  languages_variant_init ("iso_639");
+  languages_variant_init ("iso_639_3");
+#endif
+}
+
+const char *
+get_language_name (Pango2Language *language)
+{
+  languages_init ();
+
+  return (const char *) g_hash_table_lookup (language_map, language);
+}
+
+const char *
+get_language_name_for_tag (guint32 tag)
+{
+  hb_language_t lang;
+  const char *s;
+
+  lang = hb_ot_tag_to_language (tag);
+  s = hb_language_to_string (lang);
+
+  return get_language_name (pango2_language_from_string (s));
+}
diff --git a/demos/font-explorer/language-names.h b/demos/font-explorer/language-names.h
new file mode 100644
index 0000000000..4f27985dfa
--- /dev/null
+++ b/demos/font-explorer/language-names.h
@@ -0,0 +1,13 @@
+#ifndef LANGUAGE_NAMES_H
+#define LANGUAGE_NAMES_H
+
+#include <pango2/pango.h>
+
+G_BEGIN_DECLS
+
+const char * get_language_name (Pango2Language *language);
+const char * get_language_name_for_tag (guint32 tag);
+
+G_END_DECLS
+
+#endif
diff --git a/demos/font-explorer/main.c b/demos/font-explorer/main.c
new file mode 100644
index 0000000000..1a82fa5d33
--- /dev/null
+++ b/demos/font-explorer/main.c
@@ -0,0 +1,8 @@
+#include <gtk/gtk.h>
+#include <fontexplorerapp.h>
+
+int
+main (int argc, char *argv[])
+{
+  return g_application_run (G_APPLICATION (font_explorer_app_new ()), argc, argv);
+}
diff --git a/demos/font-explorer/meson.build b/demos/font-explorer/meson.build
new file mode 100644
index 0000000000..9dd3bdfa5f
--- /dev/null
+++ b/demos/font-explorer/meson.build
@@ -0,0 +1,27 @@
+fontexplorer_sources = [
+  'main.c',
+  'fontexplorerapp.c',
+  'fontexplorerwin.c',
+  'fontcontrols.c',
+  'samplechooser.c',
+  'fontcolors.c',
+  'fontfeatures.c',
+  'fontvariations.c',
+  'fontview.c',
+  'rangeedit.c',
+  'language-names.c',
+]
+
+fontexplorer_resources = gnome.compile_resources('fontexplorer_resources',
+  'fontexplorer.gresource.xml',
+  source_dir: '.',
+)
+
+executable('gtk4-font-explorer',
+  sources: [fontexplorer_sources, fontexplorer_resources],
+  c_args: common_cflags,
+  dependencies: [ libgtk_dep, demo_conf_h ],
+  include_directories: confinc,
+  link_args: extra_demo_ldflags,
+  install: true,
+)
diff --git a/demos/font-explorer/rangeedit.c b/demos/font-explorer/rangeedit.c
new file mode 100644
index 0000000000..d68dedfb4b
--- /dev/null
+++ b/demos/font-explorer/rangeedit.c
@@ -0,0 +1,173 @@
+#include "rangeedit.h"
+#include <gtk/gtk.h>
+#include <hb-ot.h>
+
+enum {
+  PROP_ADJUSTMENT = 1,
+  PROP_DEFAULT_VALUE,
+  PROP_N_CHARS,
+  NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
+
+struct _RangeEdit
+{
+  GtkWidget parent;
+
+  GtkAdjustment *adjustment;
+  GtkScale *scale;
+  GtkEntry *entry;
+  double default_value;
+  int n_chars;
+};
+
+struct _RangeEditClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE (RangeEdit, range_edit, GTK_TYPE_WIDGET);
+
+static void
+range_edit_init (RangeEdit *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+range_edit_dispose (GObject *object)
+{
+  gtk_widget_clear_template (GTK_WIDGET (object), RANGE_EDIT_TYPE);
+
+  G_OBJECT_CLASS (range_edit_parent_class)->dispose (object);
+}
+
+static void
+range_edit_get_property (GObject      *object,
+                         unsigned int  prop_id,
+                         GValue       *value,
+                         GParamSpec   *pspec)
+{
+  RangeEdit *self = RANGE_EDIT (object);
+
+  switch (prop_id)
+    {
+    case PROP_ADJUSTMENT:
+      g_value_set_object (value, self->adjustment);
+      break;
+
+    case PROP_DEFAULT_VALUE:
+      g_value_set_double (value, self->default_value);
+      break;
+
+    case PROP_N_CHARS:
+      g_value_set_int (value, self->n_chars);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+adjustment_changed (GtkAdjustment *adjustment,
+                    RangeEdit     *self)
+{
+  char *str;
+
+  str = g_strdup_printf ("%.1f", gtk_adjustment_get_value (adjustment));
+  gtk_editable_set_text (GTK_EDITABLE (self->entry), str);
+  g_free (str);
+}
+
+static void
+range_edit_set_property (GObject      *object,
+                         unsigned int  prop_id,
+                         const GValue *value,
+                         GParamSpec   *pspec)
+{
+  RangeEdit *self = RANGE_EDIT (object);
+
+  switch (prop_id)
+    {
+    case PROP_ADJUSTMENT:
+      g_set_object (&self->adjustment, g_value_get_object (value));
+      g_signal_connect (self->adjustment, "value-changed", G_CALLBACK (adjustment_changed), self);
+      adjustment_changed (self->adjustment, self);
+      break;
+
+    case PROP_DEFAULT_VALUE:
+      self->default_value = g_value_get_double (value);
+      break;
+
+    case PROP_N_CHARS:
+      self->n_chars = g_value_get_int (value);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+range_edit_constructed (GObject *object)
+{
+  RangeEdit *self = RANGE_EDIT (object);
+
+  gtk_scale_add_mark (self->scale, self->default_value, GTK_POS_TOP, NULL);
+}
+
+static void
+entry_activated (GtkEntry  *entry,
+                 RangeEdit *self)
+{
+  double value;
+  char *err = NULL;
+
+  value = g_strtod (gtk_editable_get_text (GTK_EDITABLE (entry)), &err);
+  if (err != NULL)
+    gtk_adjustment_set_value (self->adjustment, value);
+}
+
+static void
+range_edit_class_init (RangeEditClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = range_edit_dispose;
+  object_class->get_property = range_edit_get_property;
+  object_class->set_property = range_edit_set_property;
+  object_class->constructed = range_edit_constructed;
+
+  properties[PROP_ADJUSTMENT] =
+      g_param_spec_object ("adjustment", "", "",
+                           GTK_TYPE_ADJUSTMENT,
+                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  properties[PROP_DEFAULT_VALUE] =
+      g_param_spec_double ("default-value", "", "",
+                           -G_MAXDOUBLE, G_MAXDOUBLE, 0.,
+                           G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  properties[PROP_N_CHARS] =
+      g_param_spec_int ("n-chars", "", "",
+                        0, G_MAXINT, 10,
+                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  g_object_class_install_properties (G_OBJECT_CLASS (class), NUM_PROPERTIES, properties);
+
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
+                                               "/org/gtk/fontexplorer/rangeedit.ui");
+
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), RangeEdit, scale);
+  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), RangeEdit, entry);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), entry_activated);
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (class), "rangeedit");
+}
+
+RangeEdit *
+range_edit_new (void)
+{
+  return g_object_new (RANGE_EDIT_TYPE, NULL);
+}
diff --git a/demos/font-explorer/rangeedit.h b/demos/font-explorer/rangeedit.h
new file mode 100644
index 0000000000..f266452911
--- /dev/null
+++ b/demos/font-explorer/rangeedit.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+
+#define RANGE_EDIT_TYPE (range_edit_get_type ())
+#define RANGE_EDIT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), RANGE_EDIT_TYPE, RangeEdit))
+
+
+typedef struct _RangeEdit         RangeEdit;
+typedef struct _RangeEditClass    RangeEditClass;
+
+
+GType       range_edit_get_type (void);
+RangeEdit * range_edit_new      (void);
diff --git a/demos/font-explorer/rangeedit.ui b/demos/font-explorer/rangeedit.ui
new file mode 100644
index 0000000000..30b3201e7d
--- /dev/null
+++ b/demos/font-explorer/rangeedit.ui
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="RangeEdit" parent="GtkWidget">
+    <property name="layout-manager">
+      <object class="GtkBoxLayout">
+        <property name="spacing">10</property>
+      </object>
+    </property>
+    <child>
+      <object class="GtkScale" id="scale">
+        <property name="orientation">horizontal</property>
+        <property name="hexpand">1</property>
+        <property name="adjustment" bind-source="RangeEdit" bind-flags="sync-create"/>
+      </object>
+    </child>
+    <child>
+      <object class="GtkEntry" id="entry">
+        <property name="width-chars" bind-source="RangeEdit" bind-property="n-chars" 
bind-flags="sync-create"/>
+        <property name="max-width-chars" bind-source="RangeEdit" bind-property="n-chars" 
bind-flags="sync-create"/>
+        <signal name="activate" handler="entry_activated"/>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/demos/font-explorer/samplechooser.c b/demos/font-explorer/samplechooser.c
new file mode 100644
index 0000000000..6b7975e370
--- /dev/null
+++ b/demos/font-explorer/samplechooser.c
@@ -0,0 +1,162 @@
+#include "samplechooser.h"
+#include <gtk/gtk.h>
+#include <hb-ot.h>
+
+enum {
+  PROP_SAMPLE_TEXT = 1,
+  NUM_PROPERTIES
+};
+
+static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
+
+struct _SampleChooser
+{
+  GtkWidget parent;
+
+  int sample;
+  const char *sample_text;
+};
+
+struct _SampleChooserClass
+{
+  GtkWidgetClass parent_class;
+};
+
+G_DEFINE_TYPE(SampleChooser, sample_chooser, GTK_TYPE_WIDGET);
+
+static const char *pangrams[] = {
+  "The quick brown fox jumps over the lazy dog.",
+  "Waltz, bad nymph, for quick jigs vex.",
+  "Quick zephyrs blow, vexing daft Jim.",
+  "Crazy Fredrick bought many very exquisite opal jewels.",
+  "Jaded zombies acted quaintly but kept driving their oxen forward.",
+};
+
+static const char *paragraphs[] = {
+  "Grumpy wizards make toxic brew for the evil Queen and Jack. A quick movement of the enemy will jeopardize 
six gunboats. The job of waxing linoleum frequently peeves chintzy kids. My girl wove six dozen plaid jackets 
before she quit. Twelve ziggurats quickly jumped a finch box.",
+  "    Разъяренный чтец эгоистично бьёт пятью жердями шустрого фехтовальщика. Наш банк вчера же выплатил 
Ф.Я. Эйхгольду комиссию за ценные вещи. Эх, чужак, общий съём цен шляп (юфть) – вдрызг! В чащах юга жил бы 
цитрус? Да, но фальшивый экземпляр!",
+  "Τάχιστη αλώπηξ βαφής ψημένη γη, δρασκελίζει υπέρ νωθρού κυνός",
+};
+
+static const char *alphabets[] = {
+  "abcdefghijklmnopqrstuvwxyz",
+  "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
+  "0123456789",
+  "!@#$%^&*()?",
+};
+
+static const char *titles[] = {
+  "From My Cold Dead Hands",
+  "From Afar Upon the Back of a Tiger",
+  "Spontaneous Apple Creation",
+  "Big Bizness (Screwed & Chopped)",
+  "Pizza Shop Extended",
+  "Good News & Bad News",
+};
+
+static void
+next_pangram (GtkButton     *button,
+              SampleChooser *self)
+{
+  self->sample++;
+  self->sample_text = pangrams[self->sample % G_N_ELEMENTS (pangrams)];
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SAMPLE_TEXT]);
+}
+
+static void
+next_paragraph (GtkButton     *button,
+                SampleChooser *self)
+{
+  self->sample++;
+  self->sample_text = paragraphs[self->sample % G_N_ELEMENTS (paragraphs)];
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SAMPLE_TEXT]);
+}
+
+static void
+next_alphabet (GtkButton     *button,
+               SampleChooser *self)
+{
+  self->sample++;
+  self->sample_text = alphabets[self->sample % G_N_ELEMENTS (alphabets)];
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SAMPLE_TEXT]);
+}
+
+static void
+next_title (GtkButton     *button,
+            SampleChooser *self)
+{
+  self->sample++;
+  self->sample_text = titles[self->sample % G_N_ELEMENTS (titles)];
+  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SAMPLE_TEXT]);
+}
+
+static void
+sample_chooser_init (SampleChooser *self)
+{
+  self->sample_text = "Boring sample text";
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+static void
+sample_chooser_dispose (GObject *object)
+{
+  GtkWidget *child;
+
+  gtk_widget_clear_template (GTK_WIDGET (object), SAMPLE_CHOOSER_TYPE);
+
+  while ((child = gtk_widget_get_first_child (GTK_WIDGET (object))) != NULL)
+    gtk_widget_unparent (child);
+
+  G_OBJECT_CLASS (sample_chooser_parent_class)->dispose (object);
+}
+
+static void
+sample_chooser_get_property (GObject      *object,
+                             unsigned int  prop_id,
+                             GValue       *value,
+                             GParamSpec   *pspec)
+{
+  SampleChooser *self = SAMPLE_CHOOSER (object);
+
+  switch (prop_id)
+    {
+    case PROP_SAMPLE_TEXT:
+      g_value_set_string (value, self->sample_text);
+      break;
+
+    default:
+      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+    }
+}
+
+static void
+sample_chooser_class_init (SampleChooserClass *class)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+  object_class->dispose = sample_chooser_dispose;
+  object_class->get_property = sample_chooser_get_property;
+
+  properties[PROP_SAMPLE_TEXT] =
+      g_param_spec_string ("sample-text", "", "",
+                           "",
+                           G_PARAM_READABLE);
+
+  g_object_class_install_properties (G_OBJECT_CLASS (class), NUM_PROPERTIES, properties);
+
+  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
+                                               "/org/gtk/fontexplorer/samplechooser.ui");
+
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), next_pangram);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), next_paragraph);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), next_alphabet);
+  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), next_title);
+
+  gtk_widget_class_set_css_name (GTK_WIDGET_CLASS (class), "samplechooser");
+}
+
+SampleChooser *
+sample_chooser_new (void)
+{
+  return g_object_new (SAMPLE_CHOOSER_TYPE, NULL);
+}
diff --git a/demos/font-explorer/samplechooser.h b/demos/font-explorer/samplechooser.h
new file mode 100644
index 0000000000..21bf49d285
--- /dev/null
+++ b/demos/font-explorer/samplechooser.h
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <gtk/gtk.h>
+
+
+#define SAMPLE_CHOOSER_TYPE (sample_chooser_get_type ())
+#define SAMPLE_CHOOSER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SAMPLE_CHOOSER_TYPE, SampleChooser))
+
+
+typedef struct _SampleChooser         SampleChooser;
+typedef struct _SampleChooserClass    SampleChooserClass;
+
+
+GType           sample_chooser_get_type (void);
+SampleChooser * sample_chooser_new      (void);
diff --git a/demos/font-explorer/samplechooser.ui b/demos/font-explorer/samplechooser.ui
new file mode 100644
index 0000000000..de8a66c778
--- /dev/null
+++ b/demos/font-explorer/samplechooser.ui
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="SampleChooser" parent="GtkWidget">
+    <property name="layout-manager"><object class="GtkGridLayout"/></property>
+    <child>
+      <object class="GtkButton">
+        <property name="label">Pangram</property>
+        <signal name="clicked" handler="next_pangram"/>
+        <layout>
+          <property name="row">0</property>
+          <property name="column">0</property>
+        </layout>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton">
+        <property name="label">Paragraph</property>
+        <signal name="clicked" handler="next_paragraph"/>
+        <layout>
+          <property name="row">0</property>
+          <property name="column">1</property>
+        </layout>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton">
+        <property name="label">Alphabet</property>
+        <signal name="clicked" handler="next_alphabet"/>
+        <layout>
+          <property name="row">1</property>
+          <property name="column">0</property>
+        </layout>
+      </object>
+    </child>
+    <child>
+      <object class="GtkButton">
+        <property name="label">Title</property>
+        <signal name="clicked" handler="next_title"/>
+        <layout>
+          <property name="row">1</property>
+          <property name="column">1</property>
+        </layout>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/demos/meson.build b/demos/meson.build
index 91eb3c465c..c91abe06b9 100644
--- a/demos/meson.build
+++ b/demos/meson.build
@@ -22,3 +22,4 @@ subdir('icon-browser')
 subdir('node-editor')
 subdir('widget-factory')
 subdir('print-editor')
+subdir('font-explorer')


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]