chromium/ui/gfx/system_fonts_win.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gfx/system_fonts_win.h"

#include <windows.h>

#include "base/containers/flat_map.h"
#include "base/logging.h"
#include "base/no_destructor.h"
#include "base/not_fatal_until.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "base/trace_event/trace_event.h"
#include "base/win/scoped_gdi_object.h"
#include "base/win/scoped_hdc.h"
#include "base/win/scoped_select_object.h"
#include "ui/gfx/platform_font.h"

namespace gfx {
namespace win {

namespace {

class SystemFonts {
 public:
  const gfx::Font& GetFont(SystemFont system_font) {
    if (!IsInitialized())
      Initialize();

    auto it = system_fonts_.find(system_font);
    CHECK(it != system_fonts_.end(), base::NotFatalUntil::M130)
        << "System font #" << static_cast<int>(system_font) << " not found!";
    return it->second;
  }

  static SystemFonts* Instance() {
    static base::NoDestructor<SystemFonts> instance;
    return instance.get();
  }

  SystemFonts(const SystemFonts&) = delete;
  SystemFonts& operator=(const SystemFonts&) = delete;

  void ResetForTesting() {
    SystemFonts::is_initialized_ = false;
    SystemFonts::adjust_font_callback_ = nullptr;
    SystemFonts::get_minimum_font_size_callback_ = nullptr;
    system_fonts_.clear();
  }

  static int AdjustFontSize(int lf_height, int size_delta) {
    // Extract out the sign of |lf_height| - we'll add it back later.
    const int lf_sign = lf_height < 0 ? -1 : 1;
    lf_height = std::abs(lf_height);

    // Apply the size adjustment.
    lf_height += size_delta;

    // Make sure |lf_height| is not smaller than allowed min allowed font size.
    int min_font_size = 0;
    if (get_minimum_font_size_callback_) {
      min_font_size = get_minimum_font_size_callback_();
      DCHECK_GE(min_font_size, 0);
    }
    lf_height = std::max(min_font_size, lf_height);

    // Add back the sign.
    return lf_sign * lf_height;
  }

  static void AdjustLOGFONT(const FontAdjustment& font_adjustment,
                            LOGFONT* logfont) {
    DCHECK_GT(font_adjustment.font_scale, 0.0);
    LONG new_height =
        base::ClampRound<LONG>(logfont->lfHeight * font_adjustment.font_scale);
    if (logfont->lfHeight && !new_height)
      new_height = logfont->lfHeight > 0 ? 1 : -1;
    logfont->lfHeight = new_height;
    if (!font_adjustment.font_family_override.empty()) {
      auto result = wcscpy_s(logfont->lfFaceName,
                             font_adjustment.font_family_override.c_str());
      DCHECK_EQ(0, result) << "Font name "
                           << font_adjustment.font_family_override
                           << " cannot be copied into LOGFONT structure.";
    }
  }

  static Font GetFontFromLOGFONT(const LOGFONT& logfont) {
    // Finds a matching font by triggering font mapping. The font mapper finds
    // the closest physical font for a given logical font.
    base::win::ScopedHFONT font(::CreateFontIndirect(&logfont));
    base::win::ScopedGetDC screen_dc(NULL);
    base::win::ScopedSelectObject scoped_font(screen_dc, font.get());

    DCHECK(font.get()) << "Font for '"
                       << base::SysWideToUTF8(logfont.lfFaceName)
                       << "' has an invalid handle.";

    // Retrieve the name and height of the mapped font (physical font).
    LOGFONT mapped_font_info;
    GetObject(font.get(), sizeof(mapped_font_info), &mapped_font_info);
    std::string font_name = base::SysWideToUTF8(mapped_font_info.lfFaceName);

    TEXTMETRIC mapped_font_metrics;
    GetTextMetrics(screen_dc, &mapped_font_metrics);
    const int font_size =
        std::max<int>(1, mapped_font_metrics.tmHeight -
                             mapped_font_metrics.tmInternalLeading);

    Font system_font =
        Font(PlatformFont::CreateFromNameAndSize(font_name, font_size));

    // System fonts may have different styles when they are manually changed by
    // the users (see crbug.com/989476).
    Font::FontStyle style = logfont.lfItalic == 0 ? Font::FontStyle::NORMAL
                                                  : Font::FontStyle::ITALIC;
    Font::Weight weight = logfont.lfWeight == 0
                              ? Font::Weight::NORMAL
                              : static_cast<Font::Weight>(logfont.lfWeight);
    if (style != Font::FontStyle::NORMAL || weight != Font::Weight::NORMAL)
      system_font = system_font.Derive(0, style, weight);

    return system_font;
  }

  static void SetGetMinimumFontSizeCallback(
      GetMinimumFontSizeCallback callback) {
    DCHECK(!SystemFonts::IsInitialized());
    get_minimum_font_size_callback_ = callback;
  }

  static void SetAdjustFontCallback(AdjustFontCallback callback) {
    DCHECK(!SystemFonts::IsInitialized());
    adjust_font_callback_ = callback;
  }

 private:
  friend base::NoDestructor<SystemFonts>;

  SystemFonts() {}

  void Initialize() {
    TRACE_EVENT0("fonts", "gfx::SystemFonts::Initialize");

    NONCLIENTMETRICS metrics = {};
    metrics.cbSize = sizeof(metrics);
    const bool success = !!SystemParametersInfo(SPI_GETNONCLIENTMETRICS,
                                                metrics.cbSize, &metrics, 0);
    DCHECK(success);

    // NOTE(dfried): When rendering Chrome, we do all of our own font scaling
    // based on a number of factors, but what Windows reports to us has some
    // (but not all) of these factors baked in, and not in a way that is
    // display-consistent.
    //
    // For example, if your system DPI is 192 (200%) but you connect a monitor
    // with a standard DPI (100%) then even if Chrome starts on the second
    // monitor, we will be told the system font is 24pt instead of 12pt.
    // Conversely, if the system DPI is set to 96 (100%) but all of our monitors
    // are currently at 150%, Windows will still report 12pt fonts.
    //
    // The same is true with Text Zoom (a new accessibility feature). If zoom is
    // set to 150%, then Windows will report a font size of 18pt. But again, we
    // already take Text Zoom into account when rendering, so we want to account
    // for that.
    //
    // Our system fonts are in DIPs, so we must always take what Windows gives
    // us, figure out which adjustments it's making (and undo them), make our
    // own adjustments for localization (for example, we always render Hindi 25%
    // larger for readability), and only then can we store (and report) the
    // system fonts.

    // Factor in/out scale adjustment that fall outside what we can access here.
    // This includes l10n adjustments and those we have to ask UWP or other COM
    // interfaces for (since we don't have dependencies on that code from this
    // module, and don't want to implicitly invoke COM for testing purposes if
    // we don't have to).
    FontAdjustment font_adjustment;
    if (adjust_font_callback_) {
      adjust_font_callback_(font_adjustment);
    }

    // Factor out system DPI scale that Windows will include in reported font
    // sizes. Note that these are (sadly) system-wide and do not reflect
    // specific displays' DPI.
    double system_scale = GetSystemScale();
    font_adjustment.font_scale /= system_scale;

    // Grab each of the fonts from the NONCLIENTMETRICS block, adjust it
    // appropriately, and store it in the font table.
    AddFont(SystemFont::kCaption, font_adjustment, &metrics.lfCaptionFont);
    AddFont(SystemFont::kSmallCaption, font_adjustment,
            &metrics.lfSmCaptionFont);
    AddFont(SystemFont::kMenu, font_adjustment, &metrics.lfMenuFont);
    AddFont(SystemFont::kMessage, font_adjustment, &metrics.lfMessageFont);
    AddFont(SystemFont::kStatus, font_adjustment, &metrics.lfStatusFont);

    is_initialized_ = true;
  }

  static bool IsInitialized() { return is_initialized_; }

  void AddFont(SystemFont system_font,
               const FontAdjustment& font_adjustment,
               LOGFONT* logfont) {
    TRACE_EVENT0("fonts", "gfx::SystemFonts::AddFont");

    // Make adjustments to the font as necessary.
    AdjustLOGFONT(font_adjustment, logfont);

    // Cap at minimum font size.
    logfont->lfHeight = AdjustFontSize(logfont->lfHeight, 0);

    system_fonts_.emplace(system_font, GetFontFromLOGFONT(*logfont));
  }

  // Returns the system DPI scale (standard DPI being 1.0).
  // TODO(dfried): move dpi.[h|cc] somewhere in base/win so we can share this
  // logic. However, note that the similar function in dpi.h is used many places
  // it ought not to be.
  static double GetSystemScale() {
    constexpr double kDefaultDPI = 96.0;
    base::win::ScopedGetDC screen_dc(nullptr);
    return ::GetDeviceCaps(screen_dc, LOGPIXELSY) / kDefaultDPI;
  }

  // Use a flat map for faster lookups.
  base::flat_map<SystemFont, gfx::Font> system_fonts_;

  static bool is_initialized_;

  // Font adjustment callback.
  static AdjustFontCallback adjust_font_callback_;

  // Minimum size callback.
  static GetMinimumFontSizeCallback get_minimum_font_size_callback_;
};

// static
bool SystemFonts::is_initialized_ = false;

// static
AdjustFontCallback SystemFonts::adjust_font_callback_ = nullptr;

// static
GetMinimumFontSizeCallback SystemFonts::get_minimum_font_size_callback_ =
    nullptr;

}  // namespace

void SetGetMinimumFontSizeCallback(GetMinimumFontSizeCallback callback) {
  SystemFonts::SetGetMinimumFontSizeCallback(callback);
}

void SetAdjustFontCallback(AdjustFontCallback callback) {
  SystemFonts::SetAdjustFontCallback(callback);
}

const Font& GetDefaultSystemFont() {
  // The message font is the closest font for a default system font from the
  // structure NONCLIENTMETRICS. The lfMessageFont field contains information
  // about the logical font used to display text in message boxes.
  return GetSystemFont(SystemFont::kMessage);
}

const Font& GetSystemFont(SystemFont system_font) {
  return SystemFonts::Instance()->GetFont(system_font);
}

int AdjustFontSize(int lf_height, int size_delta) {
  return SystemFonts::AdjustFontSize(lf_height, size_delta);
}

void AdjustLOGFONTForTesting(const FontAdjustment& font_adjustment,
                             LOGFONT* logfont) {
  SystemFonts::AdjustLOGFONT(font_adjustment, logfont);
}

// Retrieve a FONT from a LOGFONT structure.
Font GetFontFromLOGFONTForTesting(const LOGFONT& logfont) {
  return SystemFonts::GetFontFromLOGFONT(logfont);
}

void ResetSystemFontsForTesting() {
  SystemFonts::Instance()->ResetForTesting();
}

}  // namespace win
}  // namespace gfx