Commit 42ea29e9 authored by Christian Persch's avatar Christian Persch

draw: Move FontInfo to its own file

parent bb6adce8
/*
* Copyright (C) 2003,2008 Red Hat, Inc.
* Copyright © 2019, 2020 Christian Persch
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "fonts-pangocairo.hh"
#include "debug.h"
#include "vtedefines.hh"
/* Have a space between letters to make sure ligatures aren't used when caching the glyphs: bug 793391. */
#define VTE_DRAW_SINGLE_WIDE_CHARACTERS \
" ! \" # $ % & ' ( ) * + , - . / " \
"0 1 2 3 4 5 6 7 8 9 " \
": ; < = > ? @ " \
"A B C D E F G H I J K L M N O P Q R S T U V W X Y Z " \
"[ \\ ] ^ _ ` " \
"a b c d e f g h i j k l m n o p q r s t u v w x y z " \
"{ | } ~ " \
""
static inline bool
_vte_double_equal(double a,
double b)
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wfloat-equal"
return a == b;
#pragma GCC diagnostic pop
}
#define FONT_CACHE_TIMEOUT (30) /* seconds */
namespace vte {
namespace view {
FontInfo::UnistrInfo*
FontInfo::find_unistr_info(vteunistr c)
{
if (G_LIKELY (c < G_N_ELEMENTS(m_ascii_unistr_info)))
return &m_ascii_unistr_info[c];
if (G_UNLIKELY (m_other_unistr_info == nullptr))
m_other_unistr_info = g_hash_table_new_full(nullptr, nullptr, nullptr, (GDestroyNotify)unistr_info_destroy);
auto uinfo = reinterpret_cast<UnistrInfo*>(g_hash_table_lookup(m_other_unistr_info, GINT_TO_POINTER(c)));
if (G_LIKELY (uinfo))
return uinfo;
uinfo = new UnistrInfo{};
g_hash_table_insert(m_other_unistr_info, GINT_TO_POINTER (c), uinfo);
return uinfo;
}
void
FontInfo::cache_ascii()
{
PangoLayoutLine *line;
PangoGlyphItemIter iter;
PangoGlyphItem *glyph_item;
PangoGlyphString *glyph_string;
PangoFont *pango_font;
cairo_scaled_font_t *scaled_font;
const char *text;
gboolean more;
PangoLanguage *language;
gboolean latin_uses_default_language;
/* We have m_layout holding most ASCII characters. We want to
* cache as much info as we can about the ASCII letters so we don't
* have to look them up again later */
/* Don't cache if unknown glyphs found in layout */
if (pango_layout_get_unknown_glyphs_count(m_layout.get()) != 0)
return;
language = pango_context_get_language(pango_layout_get_context(m_layout.get()));
if (language == nullptr)
language = pango_language_get_default ();
latin_uses_default_language = pango_language_includes_script (language, PANGO_SCRIPT_LATIN);
text = pango_layout_get_text(m_layout.get());
line = pango_layout_get_line_readonly(m_layout.get(), 0);
/* Don't cache if more than one font used for the line */
if (G_UNLIKELY (!line || !line->runs || line->runs->next))
return;
glyph_item = (PangoGlyphItem *)line->runs->data;
glyph_string = glyph_item->glyphs;
pango_font = glyph_item->item->analysis.font;
if (!pango_font)
return;
scaled_font = pango_cairo_font_get_scaled_font ((PangoCairoFont *) pango_font);
if (!scaled_font)
return;
for (more = pango_glyph_item_iter_init_start (&iter, glyph_item, text);
more;
more = pango_glyph_item_iter_next_cluster (&iter))
{
PangoGlyphGeometry *geometry;
PangoGlyph glyph;
vteunistr c;
/* Only cache simple clusters */
if (iter.start_char +1 != iter.end_char ||
iter.start_index+1 != iter.end_index ||
iter.start_glyph+1 != iter.end_glyph)
continue;
c = text[iter.start_index];
glyph = glyph_string->glyphs[iter.start_glyph].glyph;
geometry = &glyph_string->glyphs[iter.start_glyph].geometry;
/* If not using the default locale language, only cache non-common
* characters as common characters get their font from their neighbors
* and we don't want to force Latin on them. */
if (!latin_uses_default_language &&
g_unichar_get_script (c) <= G_UNICODE_SCRIPT_INHERITED)
continue;
/* Only cache simple glyphs */
if (!(glyph <= 0xFFFF) || (geometry->x_offset | geometry->y_offset) != 0)
continue;
auto uinfo = find_unistr_info(c);
if (G_UNLIKELY (uinfo->coverage() != UnistrInfo::Coverage::UNKNOWN))
continue;
auto ufi = &uinfo->m_ufi;
uinfo->width = PANGO_PIXELS_CEIL (geometry->width);
uinfo->has_unknown_chars = false;
uinfo->set_coverage(UnistrInfo::Coverage::USE_CAIRO_GLYPH);
ufi->using_cairo_glyph.scaled_font = cairo_scaled_font_reference (scaled_font);
ufi->using_cairo_glyph.glyph_index = glyph;
#ifdef VTE_DEBUG
m_coverage_count[0]++;
m_coverage_count[(unsigned)uinfo->coverage()]++;
#endif
}
#ifdef VTE_DEBUG
_vte_debug_print (VTE_DEBUG_PANGOCAIRO,
"vtepangocairo: %p cached %d ASCII letters\n",
(void*)this, m_coverage_count[0]);
#endif
}
void
FontInfo::measure_font()
{
PangoRectangle logical;
/* Measure U+0021..U+007E individually instead of all together and then
* averaging. For monospace fonts, the results should be the same, but
* if the user (by design, or trough mis-configuration) uses a proportional
* font, the latter method will greatly underestimate the required width,
* leading to unreadable, overlapping characters.
* https://gitlab.gnome.org/GNOME/vte/issues/138
*/
int max_width{1};
int max_height{1};
for (char c = 0x21; c < 0x7f; ++c) {
pango_layout_set_text(m_layout.get(), &c, 1);
pango_layout_get_extents(m_layout.get(), nullptr, &logical);
max_width = std::max(max_width, PANGO_PIXELS_CEIL(logical.width));
max_height = std::max(max_height, PANGO_PIXELS_CEIL(logical.height));
}
/* Use the sample text to get the baseline */
pango_layout_set_text(m_layout.get(), VTE_DRAW_SINGLE_WIDE_CHARACTERS, -1);
pango_layout_get_extents(m_layout.get(), nullptr, &logical);
/* We don't do CEIL for width since we are averaging;
* rounding is more accurate */
m_ascent = PANGO_PIXELS_CEIL(pango_layout_get_baseline(m_layout.get()));
m_height = max_height;
m_width = max_width;
/* Now that we shaped the entire ASCII character string, cache glyph
* info for them */
cache_ascii();
if (m_height == 0) {
m_height = PANGO_PIXELS_CEIL (logical.height);
}
if (m_ascent == 0) {
m_ascent = PANGO_PIXELS_CEIL(pango_layout_get_baseline(m_layout.get()));
}
_vte_debug_print (VTE_DEBUG_MISC,
"vtepangocairo: %p font metrics = %dx%d (%d)\n",
(void*)this, m_width, m_height, m_ascent);
}
FontInfo::FontInfo(PangoContext *context)
{
_vte_debug_print (VTE_DEBUG_PANGOCAIRO,
"vtepangocairo: %p allocating FontInfo\n",
(void*)this);
// FIXME: placement new
memset(m_ascii_unistr_info, 0, sizeof(m_ascii_unistr_info));
m_layout = vte::glib::take_ref(pango_layout_new(context));
auto tabs = pango_tab_array_new_with_positions(1, FALSE, PANGO_TAB_LEFT, 1);
pango_layout_set_tabs(m_layout.get(), tabs);
pango_tab_array_free(tabs);
// FIXME!!!
m_string = g_string_sized_new(VTE_UTF8_BPC+1);
measure_font();
g_hash_table_insert(s_font_info_for_context,
pango_layout_get_context(m_layout.get()),
this);
}
FontInfo::~FontInfo()
{
g_hash_table_remove(s_font_info_for_context,
pango_layout_get_context(m_layout.get()));
vteunistr i;
#ifdef VTE_DEBUG
_vte_debug_print (VTE_DEBUG_PANGOCAIRO,
"vtepangocairo: %p freeing font_info. coverages %d = %d + %d + %d\n",
(void*)this,
m_coverage_count[0],
m_coverage_count[1],
m_coverage_count[2],
m_coverage_count[3]);
#endif
g_string_free(m_string, true);
for (i = 0; i < G_N_ELEMENTS(m_ascii_unistr_info); i++)
m_ascii_unistr_info[i].~UnistrInfo();
if (m_other_unistr_info) {
g_hash_table_destroy(m_other_unistr_info);
}
}
static GQuark
fontconfig_timestamp_quark (void)
{
static GQuark quark;
if (G_UNLIKELY (!quark))
quark = g_quark_from_static_string ("vte-fontconfig-timestamp");
return quark;
}
static void
vte_pango_context_set_fontconfig_timestamp (PangoContext *context,
guint fontconfig_timestamp)
{
g_object_set_qdata ((GObject *) context,
fontconfig_timestamp_quark (),
GUINT_TO_POINTER (fontconfig_timestamp));
}
static guint
vte_pango_context_get_fontconfig_timestamp (PangoContext *context)
{
return GPOINTER_TO_UINT (g_object_get_qdata ((GObject *) context,
fontconfig_timestamp_quark ()));
}
static guint
context_hash (PangoContext *context)
{
return pango_units_from_double (pango_cairo_context_get_resolution (context))
^ pango_font_description_hash (pango_context_get_font_description (context))
^ cairo_font_options_hash (pango_cairo_context_get_font_options (context))
^ GPOINTER_TO_UINT (pango_context_get_language (context))
^ vte_pango_context_get_fontconfig_timestamp (context);
}
static gboolean
context_equal (PangoContext *a,
PangoContext *b)
{
return _vte_double_equal(pango_cairo_context_get_resolution(a), pango_cairo_context_get_resolution (b))
&& pango_font_description_equal (pango_context_get_font_description (a), pango_context_get_font_description (b))
&& cairo_font_options_equal (pango_cairo_context_get_font_options (a), pango_cairo_context_get_font_options (b))
&& pango_context_get_language (a) == pango_context_get_language (b)
&& vte_pango_context_get_fontconfig_timestamp (a) == vte_pango_context_get_fontconfig_timestamp (b);
}
// FIXMEchpe return vte::base::RefPtr<FontInfo>
/* assumes ownership/reference of context */
FontInfo*
FontInfo::find_for_context(vte::glib::RefPtr<PangoContext>& context)
{
if (G_UNLIKELY (s_font_info_for_context == nullptr))
s_font_info_for_context = g_hash_table_new((GHashFunc) context_hash, (GEqualFunc) context_equal);
auto info = reinterpret_cast<FontInfo*>(g_hash_table_lookup(s_font_info_for_context, context.get()));
if (G_LIKELY(info)) {
_vte_debug_print (VTE_DEBUG_PANGOCAIRO,
"vtepangocairo: %p found font_info in cache\n",
info);
info = info->ref();
} else {
info = new FontInfo{context.release()};
}
return info;
}
/* assumes ownership/reference of context */
FontInfo*
FontInfo::create_for_context(vte::glib::RefPtr<PangoContext> context,
PangoFontDescription const* desc,
PangoLanguage* language,
guint fontconfig_timestamp)
{
if (!PANGO_IS_CAIRO_FONT_MAP(pango_context_get_font_map(context.get()))) {
/* Ouch, Gtk+ switched over to some drawing system?
* Lets just create one from the default font map.
*/
context = vte::glib::take_ref(pango_font_map_create_context(pango_cairo_font_map_get_default()));
}
vte_pango_context_set_fontconfig_timestamp(context.get(), fontconfig_timestamp);
pango_context_set_base_dir(context.get(), PANGO_DIRECTION_LTR);
if (desc)
pango_context_set_font_description(context.get(), desc);
pango_context_set_language(context.get(), language);
/* Make sure our contexts have a font_options set. We use
* this invariant in our context hash and equal functions.
*/
if (!pango_cairo_context_get_font_options(context.get())) {
cairo_font_options_t *font_options;
font_options = cairo_font_options_create ();
pango_cairo_context_set_font_options(context.get(), font_options);
cairo_font_options_destroy (font_options);
}
return find_for_context(context);
}
FontInfo*
FontInfo::create_for_screen(GdkScreen* screen,
PangoFontDescription const* desc,
PangoLanguage* language)
{
auto settings = gtk_settings_get_for_screen(screen);
auto fontconfig_timestamp = guint{};
g_object_get (settings, "gtk-fontconfig-timestamp", &fontconfig_timestamp, nullptr);
return create_for_context(vte::glib::take_ref(gdk_pango_context_get_for_screen(screen)),
desc, language, fontconfig_timestamp);
}
FontInfo*
FontInfo::create_for_widget(GtkWidget* widget,
PangoFontDescription const* desc)
{
auto screen = gtk_widget_get_screen(widget);
auto language = pango_context_get_language(gtk_widget_get_pango_context(widget));
return create_for_screen(screen, desc, language);
}
FontInfo::UnistrInfo*
FontInfo::get_unistr_info(vteunistr c)
{
PangoRectangle logical;
PangoLayoutLine *line;
auto uinfo = find_unistr_info(c);
if (G_LIKELY (uinfo->coverage() != UnistrInfo::Coverage::UNKNOWN))
return uinfo;
auto ufi = &uinfo->m_ufi;
g_string_set_size(m_string, 0);
_vte_unistr_append_to_string(c, m_string);
pango_layout_set_text(m_layout.get(), m_string->str, m_string->len);
pango_layout_get_extents(m_layout.get(), NULL, &logical);
uinfo->width = PANGO_PIXELS_CEIL (logical.width);
line = pango_layout_get_line_readonly(m_layout.get(), 0);
uinfo->has_unknown_chars = pango_layout_get_unknown_glyphs_count(m_layout.get()) != 0;
/* we use PangoLayoutRun rendering unless there is exactly one run in the line. */
if (G_UNLIKELY (!line || !line->runs || line->runs->next))
{
uinfo->set_coverage(UnistrInfo::Coverage::USE_PANGO_LAYOUT_LINE);
ufi->using_pango_layout_line.line = pango_layout_line_ref (line);
/* we hold a manual reference on layout. pango currently
* doesn't work if line->layout is NULL. ugh! */
pango_layout_set_text(m_layout.get(), "", -1); /* make layout disassociate from the line */
ufi->using_pango_layout_line.line->layout = (PangoLayout *)g_object_ref(m_layout.get());
} else {
PangoGlyphItem *glyph_item = (PangoGlyphItem *)line->runs->data;
PangoFont *pango_font = glyph_item->item->analysis.font;
PangoGlyphString *glyph_string = glyph_item->glyphs;
/* we use fast cairo path if glyph string has only one real
* glyph and at origin */
if (!uinfo->has_unknown_chars &&
glyph_string->num_glyphs == 1 && glyph_string->glyphs[0].glyph <= 0xFFFF &&
(glyph_string->glyphs[0].geometry.x_offset |
glyph_string->glyphs[0].geometry.y_offset) == 0)
{
cairo_scaled_font_t *scaled_font = pango_cairo_font_get_scaled_font ((PangoCairoFont *) pango_font);
if (scaled_font) {
uinfo->set_coverage(UnistrInfo::Coverage::USE_CAIRO_GLYPH);
ufi->using_cairo_glyph.scaled_font = cairo_scaled_font_reference (scaled_font);
ufi->using_cairo_glyph.glyph_index = glyph_string->glyphs[0].glyph;
}
}
/* use pango fast path otherwise */
if (G_UNLIKELY (uinfo->coverage() == UnistrInfo::Coverage::UNKNOWN)) {
uinfo->set_coverage(UnistrInfo::Coverage::USE_PANGO_GLYPH_STRING);
ufi->using_pango_glyph_string.font = pango_font ? (PangoFont *)g_object_ref (pango_font) : NULL;
ufi->using_pango_glyph_string.glyph_string = pango_glyph_string_copy (glyph_string);
}
}
/* release internal layout resources */
pango_layout_set_text(m_layout.get(), "", -1);
#ifdef VTE_DEBUG
m_coverage_count[0]++;
m_coverage_count[uinfo->m_coverage]++;
#endif
return uinfo;
}
} // namespace view
} // namespace vte
/*
* Copyright (C) 2003,2008 Red Hat, Inc.
* Copyright © 2019, 2020 Christian Persch
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#pragma once
#include <cassert>
#include <glib.h>
#include <pango/pangocairo.h>
#include <gtk/gtk.h>
#include "refptr.hh"
#include "vteunistr.h"
/* Overview:
*
*
* This file implements vte rendering using pangocairo. Note that this does
* NOT implement any kind of complex text rendering. That's not currently a
* goal.
*
* The aim is to be super-fast and avoid unneeded work as much as possible.
* Here is an overview of how that is accomplished:
*
* - We attach a font_info to the draw. A font_info has all the information
* to quickly draw text.
*
* - A font_info keeps uses unistr_font_info structs that represent all
* information needed to quickly draw a single vteunistr. The font_info
* creates those unistr_font_info structs on demand and caches them
* indefinitely. It uses a direct array for the ASCII range and a hash
* table for the rest.
*
*
* Fast rendering of unistrs:
*
* A unistr_font_info (uinfo) calls Pango to set text for the unistr upon
* initialization and then caches information needed to draw the results
* later. It uses three different internal representations and respectively
* three drawing paths:
*
* - Coverage::USE_CAIRO_GLYPH:
* Keeping a single glyph index and a cairo scaled-font. This is the
* fastest way to draw text as it bypasses Pango completely and allows
* for stuffing multiple glyphs into a single cairo_show_glyphs() request
* (if scaled-fonts match). This method is used if the glyphs used for
* the vteunistr as determined by Pango consists of a single regular glyph
* positioned at 0,0 using a regular font. This method is used for more
* than 99% of the cases. Only exceptional cases fall through to the
* other two methods.
*
* - Coverage::USE_PANGO_GLYPH_STRING:
* Keeping a pango glyphstring and a pango font. This is slightly slower
* than the previous case as drawing each glyph goes through pango
* separately and causes a separate cairo_show_glyphs() call. This method
* is used when the previous method cannot be used but the glyphs for the
* character all use a single font. This is the method used for hexboxes
* and "empty" characters like U+200C ZERO WIDTH NON-JOINER for example.
*
* - Coverage::USE_PANGO_LAYOUT_LINE:
* Keeping a pango layout line. This method is used only in the very
* weird and exceptional case that a single vteunistr uses more than one
* font to be drawn. This happens for example if some diacretics is not
* available in the font chosen for the base character.
*
*
* Caching of font infos:
*
* To avoid recreating font info structs for the same font again and again we
* do the following:
*
* - Use a global cache to share font info structs across different widgets.
* We use pango language, cairo font options, resolution, and font description
* as the key for our hash table.
*
* - When a font info struct is no longer used by any widget, we delay
* destroying it for a while (FONT_CACHE_TIMEOUT seconds). This is
* supposed to serve two purposes:
*
* * Destroying a terminal widget and creating it again right after will
* reuse the font info struct from the previous widget.
*
* * Zooming in and out a terminal reuses the font info structs.
*
*
* Pre-caching ASCII letters:
*
* When initializing a font info struct we measure a string consisting of all
* ASCII letters and some other ASCII characters. Since we have a shaped pango
* layout at hand, we walk over it and cache unistr font info for the ASCII
* letters if we can do that easily using Coverage::USE_CAIRO_GLYPH. This
* means that we precache all ASCII letters without any extra pango shaping
* involved.
*/
namespace vte {
namespace view {
class DrawingContext;
class FontInfo {
friend class DrawingContext;
int const font_cache_timeout = 30; // seconds
public:
FontInfo(PangoContext* context);
~FontInfo();
FontInfo* ref()
{
// refcount is 0 when unused but still in cache
assert(m_ref_count >= 0);
++m_ref_count;
if (m_destroy_timeout != 0) {
g_source_remove (m_destroy_timeout);
m_destroy_timeout = 0;
}
return this;
}
void unref()
{
assert(m_ref_count > 0);
if (--m_ref_count > 0)
return;
/* Delay destruction by a few seconds, in case we need it again */
m_destroy_timeout = gdk_threads_add_timeout_seconds(font_cache_timeout,
(GSourceFunc)destroy_delayed_cb,
this);
}
struct UnistrInfo {
enum class Coverage : uint8_t {
/* in increasing order of speed */
UNKNOWN = 0u, /* we don't know about the character yet */
USE_PANGO_LAYOUT_LINE, /* use a PangoLayoutLine for the character */
USE_PANGO_GLYPH_STRING, /* use a PangoGlyphString for the character */
USE_CAIRO_GLYPH /* use a cairo_glyph_t for the character */
};
uint8_t m_coverage{uint8_t(Coverage::UNKNOWN)};
uint8_t has_unknown_chars;
uint16_t width;
inline constexpr Coverage coverage() const noexcept { return Coverage{m_coverage}; }
inline constexpr void set_coverage(Coverage coverage) { m_coverage = uint8_t(coverage); }
// FIXME: use std::variant<std::monostate, RefPtr<PangoLayoutLine>, ...> ?
union unistr_font_info {
/* Coverage::USE_PANGO_LAYOUT_LINE */
struct {
PangoLayoutLine *line;
} using_pango_layout_line;
/* Coverage::USE_PANGO_GLYPH_STRING */
struct {
PangoFont *font;
PangoGlyphString *glyph_string;
} using_pango_glyph_string;
/* Coverage::USE_CAIRO_GLYPH */
struct {
cairo_scaled_font_t *scaled_font;
unsigned int glyph_index;
} using_cairo_glyph;
} m_ufi;
UnistrInfo() noexcept = default;
~UnistrInfo() noexcept
{
switch (coverage()) {
default:
case Coverage::UNKNOWN:
break;
case Coverage::USE_PANGO_LAYOUT_LINE:
/* we hold a manual reference on layout */
g_object_unref (m_ufi.using_pango_layout_line.line->layout);
m_ufi.using_pango_layout_line.line->layout = NULL;
pango_layout_line_unref (m_ufi.using_pango_layout_line.line);
m_ufi.using_pango_layout_line.line = NULL;
break;
case Coverage::USE_PANGO_GLYPH_STRING:
if (m_ufi.using_pango_glyph_string.font)
g_object_unref (m_ufi.using_pango_glyph_string.font);
m_ufi.using_pango_glyph_string.font = NULL;
pango_glyph_string_free (m_ufi.using_pango_glyph_string.glyph_string);
m_ufi.using_pango_glyph_string.glyph_string = NULL;
break;
case Coverage::USE_CAIRO_GLYPH:
cairo_scaled_font_destroy (m_ufi.using_cairo_glyph.scaled_font);
m_ufi.using_cairo_glyph.scaled_font = NULL;
break;
}
}
}; // struct UnistrInfo
UnistrInfo *get_unistr_info(vteunistr c);
inline constexpr int width() const { return m_width; }
inline constexpr int height() const { return m_height; }