Commit 1de951ff authored by Daiki Ueno's avatar Daiki Ueno
Browse files

Respect OpenType font features

parent 2befc65d
......@@ -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])
......
......@@ -14,6 +14,10 @@ Gjs_MenuPopover .list-row {
padding: 5px 6px;
}
.toolbar.osd {
padding: 1px;
}
.banner {
color: @insensitive_fg_color;
}
......
......@@ -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>
......@@ -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:
*
......
......@@ -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,
......
......@@ -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();
......
......@@ -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);