gtk2/demos/gtk-demo/font_features.c
Matthias Clasen 7a05381825 gtk-demo: Port to font explorer to grouped checkbuttons
Stop using radio buttons here.
2020-08-30 17:59:23 -04:00

1377 lines
42 KiB
C

/* 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 <pango/pangofc-font.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))
static GtkWidget *the_label;
static GtkWidget *settings;
static GtkWidget *description;
static GtkWidget *font;
static GtkWidget *script_lang;
static GtkWidget *resetbutton;
static GtkWidget *stack;
static GtkWidget *the_entry;
static GtkWidget *variations_heading;
static GtkWidget *variations_grid;
static GtkWidget *instance_combo;
static GtkWidget *edit_toggle;
typedef struct {
unsigned int tag;
const char *name;
GtkWidget *icon;
GtkWidget *dflt;
GtkWidget *feat;
} FeatureItem;
static GList *feature_items;
typedef struct {
unsigned int start;
unsigned int end;
PangoFontDescription *desc;
char *features;
PangoLanguage *language;
} Range;
static GList *ranges;
static void add_font_variations (GString *s);
static void
free_range (gpointer data)
{
Range *range = data;
if (range->desc)
pango_font_description_free (range->desc);
g_free (range->features);
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,
PangoFontDescription *desc,
const char *features,
PangoLanguage *language)
{
GList *l;
Range *range;
for (l = 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;
ranges = g_list_insert_sorted (ranges, range, compare_range);
set:
if (range->desc)
pango_font_description_free (range->desc);
if (desc)
range->desc = pango_font_description_copy (desc);
g_free (range->features);
range->features = g_strdup (features);
range->language = language;
}
static const char *
get_feature_display_name (unsigned int tag)
{
int i;
static char buf[5] = { 0, };
if (tag == MAKE_TAG ('x', 'x', 'x', 'x'))
return _("Default");
for (i = 0; i < G_N_ELEMENTS (open_type_layout_features); i++)
{
if (tag == open_type_layout_features[i].tag)
return g_dpgettext2 (NULL, "OpenType layout", open_type_layout_features[i].name);
}
hb_tag_to_string (tag, buf);
g_warning ("unknown OpenType layout feature tag: %s", buf);
return buf;
}
static void update_display (void);
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;
PangoAttrList *attrs;
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);
attrs = pango_attr_list_new ();
pango_attr_list_insert (attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
gtk_label_set_attributes (GTK_LABEL (label), attrs);
pango_attr_list_unref (attrs);
gtk_box_append (GTK_BOX (group), label);
for (i = 0; tags[i]; i++)
{
unsigned int tag;
GtkWidget *feat;
FeatureItem *item;
GtkGesture *gesture;
tag = hb_tag_from_string (tags[i], -1);
feat = gtk_check_button_new_with_label (get_feature_display_name (tag));
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;
feature_items = g_list_prepend (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;
PangoAttrList *attrs;
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);
attrs = pango_attr_list_new ();
pango_attr_list_insert (attrs, pango_attr_weight_new (PANGO_WEIGHT_BOLD));
gtk_label_set_attributes (GTK_LABEL (label), attrs);
pango_attr_list_unref (attrs);
gtk_box_append (GTK_BOX (group), label);
for (i = 0; tags[i]; i++)
{
unsigned int tag;
GtkWidget *feat;
FeatureItem *item;
const 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"));
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;
feature_items = g_list_prepend (feature_items, item);
}
gtk_box_append (GTK_BOX (box), group);
}
static void
update_display (void)
{
GString *s;
const char *text;
gboolean has_feature;
GtkTreeIter iter;
GtkTreeModel *model;
PangoFontDescription *desc;
GList *l;
PangoAttrList *attrs;
PangoAttribute *attr;
int ins, bound;
guint start, end;
PangoLanguage *lang;
char *font_desc;
char *features;
text = gtk_editable_get_text (GTK_EDITABLE (the_entry));
if (gtk_label_get_selection_bounds (GTK_LABEL (the_label), &ins, &bound))
{
start = g_utf8_offset_to_pointer (text, ins) - text;
end = g_utf8_offset_to_pointer (text, bound) - text;
}
else
{
start = PANGO_ATTR_INDEX_FROM_TEXT_BEGINNING;
end = PANGO_ATTR_INDEX_TO_TEXT_END;
}
desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (font));
s = g_string_new ("");
add_font_variations (s);
if (s->len > 0)
{
pango_font_description_set_variations (desc, s->str);
g_string_free (s, TRUE);
}
font_desc = pango_font_description_to_string (desc);
s = g_string_new ("");
has_feature = FALSE;
for (l = 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);
if (gtk_combo_box_get_active_iter (GTK_COMBO_BOX (script_lang), &iter))
{
hb_tag_t lang_tag;
model = gtk_combo_box_get_model (GTK_COMBO_BOX (script_lang));
gtk_tree_model_get (model, &iter,
3, &lang_tag,
-1);
lang = pango_language_from_string (hb_language_to_string (hb_ot_tag_to_language (lang_tag)));
}
else
lang = NULL;
ensure_range (start, end, desc, features, lang);
attrs = pango_attr_list_new ();
for (l = ranges; l; l = l->next)
{
Range *range = l->data;
attr = pango_attr_font_desc_new (range->desc);
attr->start_index = range->start;
attr->end_index = range->end;
pango_attr_list_insert (attrs, attr);
attr = pango_attr_font_features_new (range->features);
attr->start_index = range->start;
attr->end_index = range->end;
pango_attr_list_insert (attrs, attr);
if (range->language)
{
attr = pango_attr_language_new (range->language);
attr->start_index = range->start;
attr->end_index = range->end;
pango_attr_list_insert (attrs, attr);
}
}
gtk_label_set_text (GTK_LABEL (description), font_desc);
gtk_label_set_text (GTK_LABEL (settings), features);
gtk_label_set_text (GTK_LABEL (the_label), text);
gtk_label_set_attributes (GTK_LABEL (the_label), attrs);
g_free (font_desc);
pango_font_description_free (desc);
g_free (features);
pango_attr_list_unref (attrs);
}
static PangoFont *
get_pango_font (void)
{
PangoFontDescription *desc;
PangoContext *context;
desc = gtk_font_chooser_get_font_desc (GTK_FONT_CHOOSER (font));
context = gtk_widget_get_pango_context (font);
return pango_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;
PangoFont *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 (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 = pango_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 = 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 == 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 (script_lang), GTK_TREE_MODEL (store));
if (have_active)
gtk_combo_box_set_active_iter (GTK_COMBO_BOX (script_lang), &active_iter);
else
gtk_combo_box_set_active_iter (GTK_COMBO_BOX (script_lang), 0);
}
static void
update_features (void)
{
int i, j;
GtkTreeModel *model;
GtkTreeIter iter;
guint script_index, lang_index;
PangoFont *pango_font;
hb_font_t *hb_font;
GList *l;
for (l = 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 (script_lang), &iter))
return;
model = gtk_combo_box_get_model (GTK_COMBO_BOX (script_lang));
gtk_tree_model_get (model, &iter,
1, &script_index,
2, &lang_index,
-1);
pango_font = get_pango_font ();
hb_font = pango_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++)
{
#if 0
char buf[5];
hb_tag_to_string (features[j], buf);
buf[4] = 0;
g_print ("%s present in %s\n", buf, i == 0 ? "GSUB" : "GPOS");
#endif
for (l = 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 (font));
if (feat)
{
for (l = 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);
typedef struct {
guint32 tag;
GtkAdjustment *adjustment;
} Axis;
static GHashTable *axes;
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, 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 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 (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_scale_set_draw_value (GTK_SCALE (axis_scale), FALSE);
gtk_grid_attach (GTK_GRID (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_grid_attach (GTK_GRID (variations_grid), axis_entry, 2, i, 1, 1);
axis = g_new (Axis, 1);
axis->tag = ax->tag;
axis->adjustment = adjustment;
g_hash_table_add (axes, axis);
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 GHashTable *instances;
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 (instances, instance);
gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (combo), instance->name);
}
static void
unset_instance (GtkAdjustment *adjustment)
{
if (instance_combo)
gtk_combo_box_set_active (GTK_COMBO_BOX (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;
PangoFont *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 (instances, &ikey);
if (!instance)
{
g_print ("did not find instance %s\n", text);
goto out;
}
pango_font = get_pango_font ();
hb_font = pango_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 (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 (axes, &key);
key.tag = MAKE_TAG('w','d','t','h');
width_axis = g_hash_table_lookup (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 (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_font_variations (void)
{
GtkWidget *child;
PangoFont *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;
child = gtk_widget_get_first_child (variations_grid);
while ((child = gtk_widget_get_first_child (variations_grid)))
gtk_grid_remove (GTK_GRID (variations_grid), child);
instance_combo = NULL;
g_hash_table_remove_all (axes);
g_hash_table_remove_all (instances);
pango_font = get_pango_font ();
hb_font = pango_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 (variations_grid), label, 0, -1, 2, 1);
combo = gtk_combo_box_text_new ();
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 (variations_grid), combo, 1, -1, 2, 1);
g_signal_connect (combo, "changed", G_CALLBACK (instance_changed), NULL);
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);
}
void
font_features_font_changed (void)
{
update_script_combo ();
update_features ();
update_font_variations ();
}
void
font_features_script_changed (void)
{
update_features ();
update_display ();
}
void
font_features_reset_features (void)
{
GList *l;
gtk_label_select_region (GTK_LABEL (the_label), 0, 0);
g_list_free_full (ranges, free_range);
ranges = NULL;
for (l = feature_items; l; l = l->next)
{
FeatureItem *item = l->data;
if (GTK_IS_RADIO_BUTTON (item->feat))
{
if (strcmp (item->name, "xxxx") == 0)
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (item->feat), TRUE);
}
else if (GTK_IS_CHECK_BUTTON (item->feat))
{
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (item->feat), FALSE);
set_inconsistent (GTK_CHECK_BUTTON (item->feat), TRUE);
}
}
}
static char *text;
static void
switch_to_entry (void)
{
text = g_strdup (gtk_editable_get_text (GTK_EDITABLE (the_entry)));
gtk_stack_set_visible_child_name (GTK_STACK (stack), "entry");
gtk_widget_grab_focus (the_entry);
}
static void
switch_to_label (void)
{
g_free (text);
text = NULL;
gtk_stack_set_visible_child_name (GTK_STACK (stack), "label");
update_display ();
}
void
font_features_toggle_edit (void)
{
if (strcmp (gtk_stack_get_visible_child_name (GTK_STACK (stack)), "label") == 0)
switch_to_entry ();
else
switch_to_label ();
}
void
font_features_stop_edit (void)
{
g_signal_emit_by_name (edit_toggle, "clicked");
}
static gboolean
entry_key_press (GtkEventController *controller,
guint keyval,
guint keycode,
GdkModifierType modifiers,
GtkEntry *entry)
{
if (keyval == GDK_KEY_Escape)
{
gtk_editable_set_text (GTK_EDITABLE (entry), text);
font_features_stop_edit ();
return GDK_EVENT_STOP;
}
return GDK_EVENT_PROPAGATE;
}
GtkWidget *
do_font_features (GtkWidget *do_widget)
{
static GtkWidget *window = NULL;
if (!window)
{
GtkBuilder *builder;
GtkWidget *feature_list;
GtkEventController *controller;
builder = gtk_builder_new_from_resource ("/font_features/font-features.ui");
window = GTK_WIDGET (gtk_builder_get_object (builder, "window"));
feature_list = GTK_WIDGET (gtk_builder_get_object (builder, "feature_list"));
the_label = GTK_WIDGET (gtk_builder_get_object (builder, "label"));
settings = GTK_WIDGET (gtk_builder_get_object (builder, "settings"));
description = GTK_WIDGET (gtk_builder_get_object (builder, "description"));
resetbutton = GTK_WIDGET (gtk_builder_get_object (builder, "reset"));
font = GTK_WIDGET (gtk_builder_get_object (builder, "font"));
script_lang = GTK_WIDGET (gtk_builder_get_object (builder, "script_lang"));
stack = GTK_WIDGET (gtk_builder_get_object (builder, "stack"));
the_entry = GTK_WIDGET (gtk_builder_get_object (builder, "entry"));
edit_toggle = GTK_WIDGET (gtk_builder_get_object (builder, "edit_toggle"));
controller = gtk_event_controller_key_new ();
g_object_set_data_full (G_OBJECT (the_entry), "controller", g_object_ref (controller), g_object_unref);
g_signal_connect (controller, "key-pressed", G_CALLBACK (entry_key_press), the_entry);
gtk_widget_add_controller (the_entry, controller);
add_check_group (feature_list, _("Kerning"), (const char *[]){ "kern", NULL });
add_check_group (feature_list, _("Ligatures"), (const char *[]){ "liga",
"dlig",
"hlig",
"clig",
"rlig", NULL });
add_check_group (feature_list, _("Letter Case"), (const char *[]){ "smcp",
"c2sc",
"pcap",
"c2pc",
"unic",
"cpsp",
"case",NULL });
add_radio_group (feature_list, _("Number Case"), (const char *[]){ "xxxx",
"lnum",
"onum", NULL });
add_radio_group (feature_list, _("Number Spacing"), (const char *[]){ "xxxx",
"pnum",
"tnum", NULL });
add_radio_group (feature_list, _("Fractions"), (const char *[]){ "xxxx",
"frac",
"afrc", NULL });
add_check_group (feature_list, _("Numeric Extras"), (const char *[]){ "zero",
"nalt",
"sinf", NULL });
add_check_group (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 (feature_list, _("Positional Alternatives"), (const char *[]){ "init",
"medi",
"med2",
"fina",
"fin2",
"fin3",
"isol", NULL });
add_check_group (feature_list, _("Width Variants"), (const char *[]){ "fwid",
"hwid",
"halt",
"pwid",
"palt",
"twid",
"qwid", NULL });
add_check_group (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 (feature_list, _("Mathematical"), (const char *[]){ "dtls",
"flac",
"mgrk",
"ssty", NULL });
add_check_group (feature_list, _("Optical Bounds"), (const char *[]){ "opbd",
"lfbd",
"rtbd", NULL });
feature_items = g_list_reverse (feature_items);
variations_heading = GTK_WIDGET (gtk_builder_get_object (builder, "variations_heading"));
variations_grid = GTK_WIDGET (gtk_builder_get_object (builder, "variations_grid"));
if (instances == NULL)
instances = g_hash_table_new_full (instance_hash, instance_equal, NULL, free_instance);
else
g_hash_table_remove_all (instances);
if (axes == NULL)
axes = g_hash_table_new_full (axes_hash, axes_equal, NULL, g_free);
else
g_hash_table_remove_all (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;
}