chromium/chrome/browser/taskbar/taskbar_decorator_win.cc

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

#include "chrome/browser/taskbar/taskbar_decorator_win.h"

#include <objbase.h>

#include <shobjidl.h>

#include <wrl/client.h>

#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/location.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/thread_pool.h"
#include "base/win/scoped_gdi_object.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/avatar_menu.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_avatar_icon_util.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "skia/ext/font_utils.h"
#include "skia/ext/image_operations.h"
#include "skia/ext/legacy_display_globals.h"
#include "skia/ext/platform_canvas.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkFont.h"
#include "third_party/skia/include/core/SkImage.h"
#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "third_party/skia/include/core/SkStream.h"
#include "ui/gfx/icon_util.h"
#include "ui/gfx/image/image.h"
#include "ui/views/win/hwnd_util.h"

namespace taskbar {

namespace {

constexpr int kOverlayIconSize = 16;

// Responsible for invoking TaskbarList::SetOverlayIcon(). The call to
// TaskbarList::SetOverlayIcon() runs a nested run loop that proves
// problematic when called on the UI thread. Additionally it seems the call may
// take a while to complete. For this reason we call it on a worker thread.
//
// Docs for TaskbarList::SetOverlayIcon() say it does nothing if the HWND is not
// valid.
void SetOverlayIcon(HWND hwnd,
                    std::unique_ptr<SkBitmap> bitmap,
                    const std::string& alt_text) {
  Microsoft::WRL::ComPtr<ITaskbarList3> taskbar;
  HRESULT result = ::CoCreateInstance(
      CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&taskbar));
  if (FAILED(result) || FAILED(taskbar->HrInit()))
    return;

  base::win::ScopedGDIObject<HICON> icon;
  if (bitmap) {
    DCHECK_GE(bitmap.get()->width(), bitmap.get()->height());

    // Maintain aspect ratio on resize, but prefer more square.
    // (We used to round down here, but rounding up produces nicer results.)
    const int resized_height = base::ClampCeil(
        kOverlayIconSize *
        (static_cast<float>(bitmap.get()->height()) / bitmap.get()->width()));

    DCHECK_GE(kOverlayIconSize, resized_height);
    // Since the target size is so small, we use our best resizer.
    SkBitmap sk_icon = skia::ImageOperations::Resize(
        *bitmap.get(), skia::ImageOperations::RESIZE_LANCZOS3, kOverlayIconSize,
        resized_height);

    // Paint the resized icon onto a 16x16 canvas otherwise Windows will badly
    // hammer it to 16x16. We'll use a circular clip to be consistent with the
    // way profile icons are rendered in the profile switcher.
    SkBitmap offscreen_bitmap;
    offscreen_bitmap.allocN32Pixels(kOverlayIconSize, kOverlayIconSize);
    SkCanvas offscreen_canvas(offscreen_bitmap, SkSurfaceProps{});
    offscreen_canvas.clear(SK_ColorTRANSPARENT);

    static const SkRRect overlay_icon_clip =
        SkRRect::MakeOval(SkRect::MakeWH(kOverlayIconSize, kOverlayIconSize));
    offscreen_canvas.clipRRect(overlay_icon_clip, true);

    // Note: the original code used kOverlayIconSize - resized_height, but in
    // order to center the icon in the circle clip area, we're going to center
    // it in the paintable region instead, rounding up to the closest pixel to
    // avoid smearing.
    const int y_offset = std::ceilf((kOverlayIconSize - resized_height) / 2.0f);
    offscreen_canvas.drawImage(sk_icon.asImage(), 0, y_offset);

    icon = IconUtil::CreateHICONFromSkBitmap(offscreen_bitmap);
    if (!icon.is_valid())
      return;
  }
  taskbar->SetOverlayIcon(hwnd, icon.get(), base::UTF8ToWide(alt_text).c_str());
}

void PostSetOverlayIcon(HWND hwnd,
                        std::unique_ptr<SkBitmap> bitmap,
                        const std::string& alt_text) {
  base::ThreadPool::CreateCOMSTATaskRunner(
      {base::MayBlock(), base::TaskPriority::USER_VISIBLE})
      ->PostTask(FROM_HERE, base::BindOnce(&SetOverlayIcon, hwnd,
                                           std::move(bitmap), alt_text));
}

}  // namespace

void DrawTaskbarDecorationString(gfx::NativeWindow window,
                                 const std::string& content,
                                 const std::string& alt_text) {
  HWND hwnd = views::HWNDForNativeWindow(window);

  // This is the color used by the Windows 10 Badge API, for platform
  // consistency.
  constexpr int kBackgroundColor = SkColorSetRGB(0x26, 0x25, 0x2D);
  constexpr int kForegroundColor = SK_ColorWHITE;
  constexpr int kRadius = kOverlayIconSize / 2;
  // The minimum gap to have between our content and the edge of the badge.
  constexpr int kMinMargin = 3;
  // The amount of space we have to render the icon.
  constexpr int kMaxBounds = kOverlayIconSize - 2 * kMinMargin;
  constexpr int kMaxTextSize = 24;  // Max size for our text.
  constexpr int kMinTextSize = 7;   // Min size for our text.

  auto badge = std::make_unique<SkBitmap>();
  badge->allocN32Pixels(kOverlayIconSize, kOverlayIconSize);

  SkCanvas canvas(*badge.get(),
                  skia::LegacyDisplayGlobals::GetSkSurfaceProps());

  SkPaint paint;
  paint.setAntiAlias(true);
  paint.setColor(kBackgroundColor);

  canvas.clear(SK_ColorTRANSPARENT);
  canvas.drawCircle(kRadius, kRadius, kRadius, paint);

  paint.reset();
  paint.setColor(kForegroundColor);

  SkFont font = skia::DefaultFont();

  SkRect bounds;
  int text_size = kMaxTextSize;
  // Find the largest |text_size| larger than |kMinTextSize| in which
  // |content| fits into our 16x16px icon, with margins.
  do {
    font.setSize(text_size--);
    font.measureText(content.c_str(), content.size(), SkTextEncoding::kUTF8,
                     &bounds);
  } while (text_size >= kMinTextSize &&
           (bounds.width() > kMaxBounds || bounds.height() > kMaxBounds));

  canvas.drawSimpleText(content.c_str(), content.size(), SkTextEncoding::kUTF8,
                        kRadius - bounds.width() / 2 - bounds.x(),
                        kRadius - bounds.height() / 2 - bounds.y(), font,
                        paint);

  PostSetOverlayIcon(hwnd, std::move(badge), alt_text);
}

void DrawTaskbarDecoration(gfx::NativeWindow window, const gfx::Image* image) {
  HWND hwnd = views::HWNDForNativeWindow(window);

  // SetOverlayIcon() does nothing if the window is not visible so testing here
  // avoids all the wasted effort of the image resizing.
  if (!::IsWindowVisible(hwnd))
    return;

  // Copy the image since we're going to use it on a separate thread and
  // gfx::Image isn't thread safe.
  std::unique_ptr<SkBitmap> bitmap;
  if (image) {
    // If `image` is an old avatar, then it's guaranteed to by 2x by code in
    // ProfileAttributesEntry::GetAvatarIcon().
    bitmap = std::make_unique<SkBitmap>(
        profiles::GetWin2xAvatarIconAsSquare(*image->ToSkBitmap()));
  }

  PostSetOverlayIcon(hwnd, std::move(bitmap), "");
}

void UpdateTaskbarDecoration(Profile* profile, gfx::NativeWindow window) {
  if (profile->IsGuestSession() ||
      // Browser process and profile manager may be null in tests.
      (g_browser_process && g_browser_process->profile_manager() &&
       g_browser_process->profile_manager()
               ->GetProfileAttributesStorage()
               .GetNumberOfProfiles() <= 1)) {
    taskbar::DrawTaskbarDecoration(window, nullptr);
    return;
  }

  // We need to draw the taskbar decoration. Even though we have an icon on the
  // window's relaunch details, we draw over it because the user may have
  // pinned the badge-less Chrome shortcut which will cause Windows to ignore
  // the relaunch details.
  // TODO(calamity): ideally this should not be necessary but due to issues
  // with the default shortcut being pinned, we add the runtime badge for
  // safety. See crbug.com/313800.
  gfx::Image decoration;
  AvatarMenu::ImageLoadStatus status = AvatarMenu::GetImageForMenuButton(
      profile->GetPath(), &decoration, kOverlayIconSize);

  // If the user is using a Gaia picture and the picture is still being loaded,
  // wait until the load finishes. This taskbar decoration will be triggered
  // again upon the finish of the picture load.
  if (status == AvatarMenu::ImageLoadStatus::LOADING ||
      status == AvatarMenu::ImageLoadStatus::PROFILE_DELETED ||
      status == AvatarMenu::ImageLoadStatus::BROWSER_SHUTTING_DOWN) {
    return;
  }

  taskbar::DrawTaskbarDecoration(window, &decoration);
}

}  // namespace taskbar