[gnome-characters/wip/dueno/font-features] Respect OpenType font features



commit 1de951ff0ff5b8cc47a4bdc984c6363cd3dc2a5e
Author: Daiki Ueno <dueno src gnome org>
Date:   Mon Aug 17 18:23:59 2015 +0900

    Respect OpenType font features

 configure.ac         |    3 +-
 data/application.css |    4 +
 data/character.ui    |  155 ++++++++++++++++++++++++++++++++++----------
 lib/gc.c             |  173 ++++++++++++++++++++++++++++++++++++++++++++++++++
 lib/gc.h             |    9 +++
 src/character.js     |  130 ++++++++++++++++++++++++++++++++++++-
 src/characterList.js |   17 ++++--
 src/window.js        |    3 +-
 8 files changed, 448 insertions(+), 46 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 4bb1348..c3a3785 100644
--- a/configure.ac
+++ b/configure.ac
@@ -40,7 +40,8 @@ PKG_CHECK_MODULES([DEPS], [gdk-3.0
                            glib-2.0
                            gobject-2.0
                            gtk+-3.0
-                           gjs-1.0 >= $GJS_MIN_VERSION])
+                           gjs-1.0 >= $GJS_MIN_VERSION
+                           harfbuzz >= 1.0.0])
 
 AC_PATH_PROG([GJS],[gjs])
 
diff --git a/data/application.css b/data/application.css
index 8047ea3..2ac3e12 100644
--- a/data/application.css
+++ b/data/application.css
@@ -14,6 +14,10 @@ Gjs_MenuPopover .list-row {
     padding: 5px 6px;
 }
 
+.toolbar.osd {
+    padding: 1px;
+}
+
 .banner {
     color: @insensitive_fg_color;
 }
diff --git a/data/character.ui b/data/character.ui
index b123d76..a098ec7 100644
--- a/data/character.ui
+++ b/data/character.ui
@@ -4,28 +4,109 @@
   <template class="Gjs_CharacterDialog" parent="GtkDialog">
     <child internal-child="vbox">
       <object class="GtkBox" id="vbox1">
+       <property name="halign">fill</property>
+       <property name="hexpand">True</property>
        <child>
          <object class="GtkStack" id="main-stack">
            <property name="visible">True</property>
+           <property name="halign">fill</property>
+           <property name="hexpand">True</property>
            <child>
              <object class="GtkGrid" id="character-grid">
                <property name="visible">True</property>
                <property name="can_focus">False</property>
-               <property name="halign">center</property>
-               <property name="valign">center</property>
+               <property name="hexpand">True</property>
+               <property name="vexpand">True</property>
+               <property name="valign">fill</property>
                <property name="border_width">5</property>
                <property name="orientation">vertical</property>
                <property name="row_spacing">50</property>
                <child>
-                 <object class="GtkLabel" id="character-label">
-                   <property name="visible">True</property>
-                   <property name="can_focus">False</property>
-                   <property name="ellipsize">end</property>
-                   <property name="halign">center</property>
-                   <property name="valign">center</property>
-                   <style>
-                     <class name="character-label"/>
-                   </style>
+                 <object class="GtkEventBox" id="eventbox">
+                   <child>
+                     <object class="GtkOverlay" id="character-overlay">
+                       <property name="visible">True</property>
+                       <property name="hexpand">True</property>
+                       <property name="vexpand">True</property>
+                       <child>
+                         <object class="GtkLabel" id="character-label">
+                           <property name="visible">True</property>
+                           <property name="can_focus">False</property>
+                           <property name="hexpand">True</property>
+                           <property name="vexpand">True</property>
+                           <property name="valign">center</property>
+                           <property name="margin-top">34</property>
+                           <style>
+                             <class name="character-label"/>
+                           </style>
+                         </object>
+                       </child>
+                       <child type="overlay">
+                         <object class="GtkRevealer" id="revealer">
+                           <property name="valign">start</property>
+                           <child>
+                             <object class="GtkToolbar" id="toolbar">
+                               <property name="opacity">0.4</property>
+                               <property name="margin">2</property>
+                               <property name="halign">center</property>
+                               <property name="valign">end</property>
+                               <property name="hexpand">False</property>
+                               <property name="receives_default">True</property>
+                               <style>
+                                 <class name="osd"/>
+                               </style>
+                               <child>
+                                 <object class="GtkToolItem">
+                                   <property name="visible">True</property>
+                                   <child>
+                                     <object class="GtkBox">
+                                       <property name="visible">True</property>
+                                       <style>
+                                         <class name="linked"/>
+                                       </style>
+                                       <child>
+                                         <object class="GtkButton" id="previous-button">
+                                           <property name="visible">True</property>
+                                           <style>
+                                             <class name="image-button"/>
+                                           </style>
+                                           <child>
+                                             <object class="GtkImage">
+                                               <property name="visible">True</property>
+                                               <property name="icon-name">pan-start-symbolic</property>
+                                               <property name="icon-size">1</property>
+                                             </object>
+                                           </child>
+                                         </object>
+                                       </child>
+                                       <child>
+                                         <object class="GtkButton" id="next-button">
+                                           <property name="visible">True</property>
+                                           <style>
+                                             <class name="image-button"/>
+                                           </style>
+                                           <child>
+                                             <object class="GtkImage">
+                                               <property name="visible">True</property>
+                                               <property name="icon-name">pan-end-symbolic</property>
+                                               <property name="icon-size">1</property>
+                                             </object>
+                                           </child>
+                                         </object>
+                                       </child>
+                                     </object>
+                                   </child>
+                                 </object>
+                               </child>
+                             </object>
+                           </child>
+                         </object>
+                         <packing>
+                           <property name="pass-through">True</property>
+                         </packing>
+                       </child>
+                     </object>
+                   </child>
                  </object>
                  <packing>
                    <property name="left_attach">0</property>
@@ -38,6 +119,8 @@
                    <property name="visible">True</property>
                    <property name="can_focus">True</property>
                    <property name="receives_default">False</property>
+                   <property name="hexpand">False</property>
+                   <property name="halign">center</property>
                  </object>
                  <packing>
                    <property name="left_attach">0</property>
@@ -49,7 +132,9 @@
                    <property name="visible">True</property>
                    <property name="can_focus">False</property>
                    <property name="halign">center</property>
+                   <property name="valign">center</property>
                    <property name="selectable">True</property>
+                   <property name="justify">center</property>
                    <style>
                      <class name="detail-label"/>
                    </style>
@@ -65,39 +150,39 @@
              </packing>
            </child>
            <child>
-             <object class="GtkScrolledWindow" id="related-scrolled">
-               <property name="visible">True</property>
-               <property name="can_focus">False</property>
-               <property name="margin_start">6</property>
-               <property name="margin_end">6</property>
-               <property name="margin_top">6</property>
-               <property name="margin_bottom">6</property>
-               <property name="hscrollbar_policy">never</property>
-               <property name="vexpand">True</property>
-               <style>
-                 <class name="related"/>
-               </style>
+         <object class="GtkScrolledWindow" id="related-scrolled">
+           <property name="visible">True</property>
+           <property name="can_focus">False</property>
+           <property name="margin_start">6</property>
+           <property name="margin_end">6</property>
+           <property name="margin_top">6</property>
+           <property name="margin_bottom">6</property>
+           <property name="hscrollbar_policy">never</property>
+           <property name="vexpand">True</property>
+           <style>
+             <class name="related"/>
+           </style>
+           <child>
+             <object class="GtkViewport" id="related-viewport">
                <child>
-                 <object class="GtkViewport" id="related-viewport">
+                 <object class="GtkListBox" id="related-listbox">
+                   <property name="visible">True</property>
+                   <property name="can_focus">False</property>
                    <child>
-                     <object class="GtkListBox" id="related-listbox">
-                       <property name="visible">True</property>
-                       <property name="can_focus">False</property>
-                       <child>
-                         <placeholder/>
-                       </child>
-                     </object>
+                     <placeholder/>
                    </child>
                  </object>
                </child>
              </object>
-             <packing>
-               <property name="name">related</property>
-             </packing>
            </child>
          </object>
+         <packing>
+           <property name="name">related</property>
+         </packing>
        </child>
       </object>
     </child>
-  </template>
+  </object>
+</child>
+</template>
 </interface>
diff --git a/lib/gc.c b/lib/gc.c
index 9f4e3be..b02a3ca 100644
--- a/lib/gc.c
+++ b/lib/gc.c
@@ -17,6 +17,8 @@
 
 #define PANGO_ENABLE_ENGINE 1
 #include <pango/pangofc-font.h>
+#include <harfbuzz/hb-ft.h>
+#include <harfbuzz/hb-ot.h>
 
 static const uc_block_t *all_blocks;
 static size_t all_block_count;
@@ -926,6 +928,177 @@ gc_pango_context_font_has_glyph (PangoContext *context,
   return retval == 0;
 }
 
+void
+gc_pango_layout_set_font_features (PangoLayout *layout, gchar *features)
+{
+  PangoAttrList *attr_list;
+
+  attr_list = pango_layout_get_attributes (layout);
+  if (!attr_list)
+    {
+      attr_list = pango_attr_list_new ();
+      pango_layout_set_attributes (layout, attr_list);
+    }
+  pango_attr_list_change (attr_list, pango_attr_font_features_new (features));
+}
+
+/**
+ * gc_pango_list_font_features:
+ * @font: a #PangoFont
+ *
+ * Returns: (transfer full) (nullable) (array zero-terminated=1): A
+ *   list of OpenType feature tags.
+ */
+gchar **
+gc_pango_list_font_features (PangoFont *font)
+{
+#ifdef HAVE_PANGOFT2
+  if (PANGO_IS_FC_FONT (font))
+    {
+      FT_Face ftface = pango_fc_font_lock_face (PANGO_FC_FONT (font));
+      hb_face_t *hbface = hb_ft_face_create_cached (ftface);
+      unsigned int count, i;
+      hb_tag_t *features;
+      gchar buf[5];
+      gchar **result;
+
+      count = hb_ot_layout_table_get_feature_tags (hbface, HB_OT_TAG_GSUB, 0,
+                                                  NULL, NULL);
+      features = g_new (hb_tag_t, count + 1);
+      hb_ot_layout_table_get_feature_tags (hbface, HB_OT_TAG_GSUB, 0,
+                                          &count, features);
+      result = g_new0 (gchar *, count + 1);
+      for (i = 0; i < count; i++)
+       {
+         buf[4] = '\0';
+         hb_tag_to_string (features[i], buf);
+         result[i] = g_strdup (buf);
+       }
+      g_free (features);
+      pango_fc_font_unlock_face (PANGO_FC_FONT (font));
+      return result;
+    }
+#endif
+  return NULL;
+}
+
+static void
+collect_features (gpointer key,
+                 gpointer value,
+                 gpointer user_data)
+{
+  GArray *result = user_data;
+  g_array_append_val (result, value);
+}
+
+/**
+ * gc_pango_list_effective_font_features:
+ * @font: a #PangoFont
+ * @font_features: (array zero-terminated=1) (element-type utf8): a
+ *   list of font features
+ * @uc: a #gunichar
+ *
+ * Returns: (transfer full) (nullable) (array zero-terminated=1): A
+ *   list of OpenType feature tags followed by a space and the index.
+ */
+gchar **
+gc_pango_list_effective_font_features (PangoFont *font,
+                                      gchar **font_features,
+                                      gunichar uc)
+{
+  GHashTable *table = g_hash_table_new (g_direct_hash, g_direct_equal);
+  GArray *result = g_array_sized_new (TRUE, FALSE,
+                                     sizeof (gchar *),
+                                     g_strv_length (font_features));
+
+#ifdef HAVE_PANGOFT2
+  if (PANGO_IS_FC_FONT (font))
+    {
+      FT_Face ftface = pango_fc_font_lock_face (PANGO_FC_FONT (font));
+      hb_face_t *hbface = hb_ft_face_create_cached (ftface);
+      hb_buffer_t *buffer;
+      hb_font_t *hbfont = NULL;
+      hb_glyph_info_t *infos;
+      unsigned int length;
+      hb_feature_t features[1];
+      hb_codepoint_t base_gid = 0;
+      gchar **p, *last;
+      gint index;
+      uint8_t utf8[6];
+      size_t utf8_length = G_N_ELEMENTS (utf8);
+
+      u32_to_u8 (&uc, 1, utf8, &utf8_length);
+
+      hbfont = hb_font_create (hbface);
+      hb_ft_font_set_funcs (hbfont);
+      hb_font_set_scale (hbfont, 10, 10);
+
+      buffer = hb_buffer_create ();
+      hb_buffer_set_direction (buffer, HB_DIRECTION_LTR);
+      hb_buffer_add_utf8 (buffer, (const char *) utf8, utf8_length, 0, 1);
+
+      hb_shape (hbfont, buffer, NULL, 0);
+      infos = hb_buffer_get_glyph_infos (buffer, &length);
+      if (length > 0)
+       base_gid = infos[0].codepoint;
+      hb_buffer_destroy (buffer);
+
+      last = NULL;
+      index = 0;
+      for (p = font_features; *p; p++)
+       {
+         gchar *feature_string;
+         hb_codepoint_t gid = 0;
+
+         /* OpenType features which could produce alternative glyphs:
+            calt, cv00-99, jp04, jp78, jp83, jp90, salt, ss00-20 */
+         if (!(strlen (*p) == 4
+               && (strcmp (*p, "calt") == 0
+                   || (strcmp (*p, "cv00") >= 0 && strcmp (*p, "cv99") <= 0)
+                   || strcmp (*p, "jp78") == 0
+                   || strcmp (*p, "jp83") == 0
+                   || strcmp (*p, "jp90") == 0
+                   || strcmp (*p, "jp04") == 0
+                   || strcmp (*p, "salt") == 0
+                   || (strcmp (*p, "ss00") >= 0 && strcmp (*p, "ss20") <= 0))))
+           continue;
+
+         if (last != NULL && strcmp (*p, last) == 0)
+           index++;
+         else
+           index = 0;
+         last = *p;
+
+         feature_string = g_strdup_printf ("%s %d", *p, index);
+
+         hb_feature_from_string (feature_string, 6, &features[0]);
+
+         buffer = hb_buffer_create ();
+         hb_buffer_set_direction (buffer, HB_DIRECTION_LTR);
+         hb_buffer_add_utf8 (buffer, (const char *) utf8, utf8_length, 0, 1);
+         hb_shape (hbfont, buffer, features, 1);
+         infos = hb_buffer_get_glyph_infos (buffer, &length);
+
+         if (length > 0)
+           gid = infos[0].codepoint;
+         hb_buffer_destroy (buffer);
+
+         if (gid != base_gid
+             && !g_hash_table_contains (table, GINT_TO_POINTER (gid)))
+           g_hash_table_insert (table, GINT_TO_POINTER (gid), feature_string);
+         else
+           g_free (feature_string);
+       }
+
+      hb_font_destroy (hbfont);
+      pango_fc_font_unlock_face (PANGO_FC_FONT (font));
+    }
+#endif
+  g_hash_table_foreach (table, collect_features, result);
+  g_hash_table_unref (table);
+  return (gchar **) g_array_free (result, FALSE);
+}
+
 /**
  * gc_get_current_language:
  *
diff --git a/lib/gc.h b/lib/gc.h
index 688c3a3..a68716d 100644
--- a/lib/gc.h
+++ b/lib/gc.h
@@ -72,6 +72,15 @@ GtkClipboard   *gc_gtk_clipboard_get      (void);
 /* Pango support.  PangoAttrFallback is not accessible from GI.  */
 void            gc_pango_layout_disable_fallback
                                           (PangoLayout          *layout);
+void            gc_pango_layout_set_font_features
+                                          (PangoLayout          *layout,
+                                          gchar                *features);
+gchar         **gc_pango_list_font_features
+                                          (PangoFont            *font);
+gchar         **gc_pango_list_effective_font_features
+                                          (PangoFont            *font,
+                                          gchar               **font_features,
+                                          gunichar              uc);
 
 gboolean        gc_pango_context_font_has_glyph
                                           (PangoContext         *context,
diff --git a/src/character.js b/src/character.js
index d39e663..768336b 100644
--- a/src/character.js
+++ b/src/character.js
@@ -21,22 +21,27 @@ const Params = imports.params;
 const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
+const Gdk = imports.gi.Gdk;
 const Gtk = imports.gi.Gtk;
 const Pango = imports.gi.Pango;
 const Gc = imports.gi.Gc;
 const Main = imports.main;
 const Util = imports.util;
+const Mainloop = imports.mainloop;
+const _AUTO_HIDE_TIMEOUT = 1;
 
 const CharacterDialog = new Lang.Class({
     Name: 'CharacterDialog',
     Extends: Gtk.Dialog,
     Template: 'resource:///org/gnome/Characters/character.ui',
-    InternalChildren: ['main-stack', 'character-label', 'detail-label',
-                       'copy-button', 'related-listbox'],
+    InternalChildren: ['main-stack', 'eventbox', 'revealer', 'toolbar',
+                       'previous-button', 'next-button', 'character-label',
+                       'detail-label', 'copy-button', 'related-listbox'],
 
     _init: function(params) {
         let filtered = Params.filter(params, { character: null,
-                                               fontDescription: null });
+                                               fontDescription: null,
+                                               fontFeatures: null });
         params = Params.fill(params, { use_header_bar: true,
                                        width_request: 400,
                                        height_request: 400 });
@@ -64,6 +69,122 @@ const CharacterDialog = new Lang.Class({
 
         this._character_label.override_font(filtered.fontDescription);
         this._setCharacter(filtered.character);
+
+        this._fontFeaturesIndex = 0;
+        this._fontFeatures = [];
+
+        if (filtered.fontFeatures.length > 0) {
+            let context = this.get_pango_context();
+            let font = context.load_font(filtered.fontDescription);
+            let effective =
+                Gc.pango_list_effective_font_features (font,
+                                                       filtered.fontFeatures,
+                                                       filtered.character);
+            if (effective.length > 0) {
+                this._eventbox.connect(
+                    'enter-notify-event',
+                    Lang.bind(this, this._handleInitialEnterNotify));
+                this._eventbox.connect(
+                    'leave-notify-event',
+                    Lang.bind(this, this._handleLeaveNotify));
+
+                this._previous_button.connect('clicked', Lang.bind(this, this._previousButtonClicked));
+                this._next_button.connect('clicked', Lang.bind(this, this._nextButtonClicked));
+
+                let widgets = [this._toolbar, this._previous_button, this._next_button];
+                for (let index in widgets) {
+                    widgets[index].connect('enter-notify-event',
+                                           Lang.bind(this, this._handleEnterNotify));
+                    widgets[index].connect('leave-notify-event',
+                                           Lang.bind(this, this._handleLeaveNotify));
+                }
+                this._fontFeatures = this._fontFeatures.concat(effective);
+                this._fontFeatures.unshift('');
+            }
+        }
+
+        this._autoHideId = 0;
+        this._previous_button.set_sensitive(false);
+        if (this._fontFeatures.length > 0)
+            this._next_button.set_sensitive(true);
+    },
+
+    _autoHide: function() {
+        this._autoHideId = 0;
+        this._revealer.set_reveal_child(false);
+        return false;
+    },
+
+    _unqueueAutoHide: function() {
+        if (this._autoHideId == 0)
+            return;
+        Mainloop.source_remove(this._autoHideId);
+        this._autoHideId = 0;
+    },
+
+    _queueAutoHide: function() {
+        this._unqueueAutoHide();
+        this._autoHideId = Mainloop.timeout_add_seconds(_AUTO_HIDE_TIMEOUT, Lang.bind(this, this._autoHide));
+    },
+
+    _handleInitialEnterNotify: function(box, event) {
+        if (this._revealer.get_child_revealed())
+            return false;
+
+        this._revealer.set_reveal_child(true);
+        return true;
+    },
+
+    _handleEnterNotify: function(widget, event) {
+        this._unqueueAutoHide();
+        return false;
+    },
+
+    _handleLeaveNotify: function(widget, event) {
+        this._queueAutoHide();
+        return false;
+    },
+
+    _changeFeature: function(feature) {
+        this._character_label.set_attributes(null);
+        let layout = this._character_label.get_layout();
+        Gc.pango_layout_set_font_features(layout, feature);
+        if (feature == '')
+            this._detail_label.label = this._codePointLabel + "\n";
+        else
+            this._detail_label.label = this._codePointLabel + "\n" + _("OpenType feature: 
%s").format(feature);
+    },
+
+    _previousButtonClicked: function(event) {
+        if (this._fontFeaturesIndex == 0)
+            return;
+
+        this._fontFeaturesIndex--;
+        if (this._fontFeaturesIndex == 0) {
+            this._previous_button.set_sensitive(false);
+            // Making the button insensitive causes leave-notify and
+            // then auto-hide of the toolbar.  Cancel it manually.
+            this._unqueueAutoHide();
+        }
+
+        this._next_button.set_sensitive(true);
+        this._changeFeature(this._fontFeatures[this._fontFeaturesIndex]);
+    },
+
+    _nextButtonClicked: function(event) {
+        if (this._fontFeaturesIndex == this._fontFeatures.length - 1)
+            return;
+
+        this._fontFeaturesIndex++;
+        if (this._fontFeaturesIndex == this._fontFeatures.length - 1) {
+            this._next_button.set_sensitive(false);
+            // Making the button insensitive causes leave-notify and
+            // then auto-hide of the toolbar.  Cancel it manually.
+            this._unqueueAutoHide();
+        }
+
+        this._previous_button.set_sensitive(true);
+        this._changeFeature(this._fontFeatures[this._fontFeaturesIndex]);
     },
 
     _finishSearch: function(result) {
@@ -111,7 +232,8 @@ const CharacterDialog = new Lang.Class({
 
         let codePoint = Util.toCodePoint(this._character);
         let codePointHex = codePoint.toString(16).toUpperCase();
-        this._detail_label.label = _("Unicode U+%04s").format(codePointHex);
+        this._codePointLabel = _("Unicode U+%04s").format(codePointHex)
+        this._detail_label.label = this._codePointLabel + "\n";
 
         this._cancellable.cancel();
         this._cancellable.reset();
diff --git a/src/characterList.js b/src/characterList.js
index 2f79f43..ba1a5af 100644
--- a/src/characterList.js
+++ b/src/characterList.js
@@ -317,20 +317,27 @@ const CharacterListView = new Lang.Class({
         return this._fontDescription;
     },
 
+    getFontFeatures: function() {
+        return this._fontFeatures;
+    },
+
     updateCharacterList: function() {
         let characters = this._characters;
-        let fontDescription = this._fontDescription;
+        let fontDescription = this._filterFontDescription ?
+            this._filterFontDescription : this._fontDescription;
+
+        let context = this.get_pango_context();
+        let font = context.load_font(fontDescription);
+        this._fontFeatures = Gc.pango_list_font_features(font);
+
         if (this._filterFontDescription) {
-            let context = this.get_pango_context();
-            let filterFont = context.load_font(this._filterFontDescription);
             let filteredCharacters = [];
             for (let index = 0; index < characters.length; index++) {
                 let uc = characters[index];
-                if (Gc.pango_context_font_has_glyph(context, filterFont, uc))
+                if (Gc.pango_context_font_has_glyph(context, font, uc))
                     filteredCharacters.push(uc);
             }
             characters = filteredCharacters;
-            fontDescription = this._filterFontDescription;
         }
 
         this._characterList.setFontDescription(fontDescription);
diff --git a/src/window.js b/src/window.js
index cfc2f05..83eab2d 100644
--- a/src/window.js
+++ b/src/window.js
@@ -350,7 +350,8 @@ const MainView = new Lang.Class({
             character: uc,
             modal: true,
             transient_for: this.get_toplevel(),
-            fontDescription: this.visible_child.getFontDescription()
+            fontDescription: this.visible_child.getFontDescription(),
+            fontFeatures: this.visible_child.getFontFeatures()
         });
 
         dialog.show();


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