859 lines
		
	
	
	
		
			24 KiB
			
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			859 lines
		
	
	
	
		
			24 KiB
			
		
	
	
	
		
			C++
		
	
	
	
	
	
// Copyright (c) 2015 GitHub, Inc.
 | 
						|
// Use of this source code is governed by the MIT license that can be
 | 
						|
// found in the LICENSE file.
 | 
						|
 | 
						|
#ifndef NOMINMAX
 | 
						|
#define NOMINMAX
 | 
						|
#endif
 | 
						|
#include "atom/browser/notifications/win/win32_desktop_notifications/toast.h"
 | 
						|
 | 
						|
#include <combaseapi.h>
 | 
						|
 | 
						|
#include <UIAutomation.h>
 | 
						|
#include <uxtheme.h>
 | 
						|
#include <windowsx.h>
 | 
						|
#include <algorithm>
 | 
						|
#include <cmath>
 | 
						|
#include <memory>
 | 
						|
 | 
						|
#include "atom/browser/notifications/win/win32_desktop_notifications/common.h"
 | 
						|
#include "atom/browser/notifications/win/win32_desktop_notifications/toast_uia.h"
 | 
						|
#include "base/logging.h"
 | 
						|
 | 
						|
#pragma comment(lib, "msimg32.lib")
 | 
						|
#pragma comment(lib, "uxtheme.lib")
 | 
						|
 | 
						|
using std::min;
 | 
						|
using std::shared_ptr;
 | 
						|
 | 
						|
namespace atom {
 | 
						|
 | 
						|
static COLORREF GetAccentColor() {
 | 
						|
  bool success = false;
 | 
						|
  if (IsAppThemed()) {
 | 
						|
    HKEY hkey;
 | 
						|
    if (RegOpenKeyEx(HKEY_CURRENT_USER,
 | 
						|
                     TEXT("SOFTWARE\\Microsoft\\Windows\\DWM"), 0,
 | 
						|
                     KEY_QUERY_VALUE, &hkey) == ERROR_SUCCESS) {
 | 
						|
      COLORREF color;
 | 
						|
      DWORD type, size;
 | 
						|
      if (RegQueryValueEx(hkey, TEXT("AccentColor"), nullptr, &type,
 | 
						|
                          reinterpret_cast<BYTE*>(&color),
 | 
						|
                          &(size = sizeof(color))) == ERROR_SUCCESS &&
 | 
						|
          type == REG_DWORD) {
 | 
						|
        // convert from RGBA
 | 
						|
        color = RGB(GetRValue(color), GetGValue(color), GetBValue(color));
 | 
						|
        success = true;
 | 
						|
      } else if (RegQueryValueEx(hkey, TEXT("ColorizationColor"), nullptr,
 | 
						|
                                 &type, reinterpret_cast<BYTE*>(&color),
 | 
						|
                                 &(size = sizeof(color))) == ERROR_SUCCESS &&
 | 
						|
                 type == REG_DWORD) {
 | 
						|
        // convert from BGRA
 | 
						|
        color = RGB(GetBValue(color), GetGValue(color), GetRValue(color));
 | 
						|
        success = true;
 | 
						|
      }
 | 
						|
 | 
						|
      RegCloseKey(hkey);
 | 
						|
 | 
						|
      if (success)
 | 
						|
        return color;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  return GetSysColor(COLOR_ACTIVECAPTION);
 | 
						|
}
 | 
						|
 | 
						|
// Stretches a bitmap to the specified size, preserves alpha channel
 | 
						|
static HBITMAP StretchBitmap(HBITMAP bitmap, unsigned width, unsigned height) {
 | 
						|
  // We use StretchBlt for the scaling, but that discards the alpha channel.
 | 
						|
  // So we first create a separate grayscale bitmap from the alpha channel,
 | 
						|
  // scale that separately, and copy it back to the scaled color bitmap.
 | 
						|
 | 
						|
  BITMAP bm;
 | 
						|
  if (!GetObject(bitmap, sizeof(bm), &bm))
 | 
						|
    return NULL;
 | 
						|
 | 
						|
  if (width == 0 || height == 0)
 | 
						|
    return NULL;
 | 
						|
 | 
						|
  HBITMAP result_bitmap = NULL;
 | 
						|
 | 
						|
  HDC hdc_screen = GetDC(NULL);
 | 
						|
 | 
						|
  HBITMAP alpha_src_bitmap;
 | 
						|
  {
 | 
						|
    BITMAPINFOHEADER bmi = {sizeof(BITMAPINFOHEADER)};
 | 
						|
    bmi.biWidth = bm.bmWidth;
 | 
						|
    bmi.biHeight = bm.bmHeight;
 | 
						|
    bmi.biPlanes = bm.bmPlanes;
 | 
						|
    bmi.biBitCount = bm.bmBitsPixel;
 | 
						|
    bmi.biCompression = BI_RGB;
 | 
						|
 | 
						|
    void* alpha_src_bits;
 | 
						|
    alpha_src_bitmap =
 | 
						|
        CreateDIBSection(NULL, reinterpret_cast<BITMAPINFO*>(&bmi),
 | 
						|
                         DIB_RGB_COLORS, &alpha_src_bits, NULL, 0);
 | 
						|
 | 
						|
    if (alpha_src_bitmap) {
 | 
						|
      if (GetDIBits(hdc_screen, bitmap, 0, 0, 0,
 | 
						|
                    reinterpret_cast<BITMAPINFO*>(&bmi), DIB_RGB_COLORS) &&
 | 
						|
          bmi.biSizeImage > 0 && (bmi.biSizeImage % 4) == 0) {
 | 
						|
        auto* buf = reinterpret_cast<BYTE*>(
 | 
						|
            _aligned_malloc(bmi.biSizeImage, sizeof(DWORD)));
 | 
						|
 | 
						|
        if (buf) {
 | 
						|
          GetDIBits(hdc_screen, bitmap, 0, bm.bmHeight, buf,
 | 
						|
                    reinterpret_cast<BITMAPINFO*>(&bmi), DIB_RGB_COLORS);
 | 
						|
 | 
						|
          const DWORD* src = reinterpret_cast<DWORD*>(buf);
 | 
						|
          const DWORD* end = reinterpret_cast<DWORD*>(buf + bmi.biSizeImage);
 | 
						|
 | 
						|
          BYTE* dest = reinterpret_cast<BYTE*>(alpha_src_bits);
 | 
						|
 | 
						|
          for (; src != end; ++src, ++dest) {
 | 
						|
            BYTE a = *src >> 24;
 | 
						|
            *dest++ = a;
 | 
						|
            *dest++ = a;
 | 
						|
            *dest++ = a;
 | 
						|
          }
 | 
						|
 | 
						|
          _aligned_free(buf);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (alpha_src_bitmap) {
 | 
						|
    BITMAPINFOHEADER bmi = {sizeof(BITMAPINFOHEADER)};
 | 
						|
    bmi.biWidth = width;
 | 
						|
    bmi.biHeight = height;
 | 
						|
    bmi.biPlanes = 1;
 | 
						|
    bmi.biBitCount = 32;
 | 
						|
    bmi.biCompression = BI_RGB;
 | 
						|
 | 
						|
    void* color_bits;
 | 
						|
    auto* color_bitmap =
 | 
						|
        CreateDIBSection(NULL, reinterpret_cast<BITMAPINFO*>(&bmi),
 | 
						|
                         DIB_RGB_COLORS, &color_bits, NULL, 0);
 | 
						|
 | 
						|
    void* alpha_bits;
 | 
						|
    auto* alpha_bitmap =
 | 
						|
        CreateDIBSection(NULL, reinterpret_cast<BITMAPINFO*>(&bmi),
 | 
						|
                         DIB_RGB_COLORS, &alpha_bits, NULL, 0);
 | 
						|
 | 
						|
    HDC hdc = CreateCompatibleDC(NULL);
 | 
						|
    HDC hdc_src = CreateCompatibleDC(NULL);
 | 
						|
 | 
						|
    if (color_bitmap && alpha_bitmap && hdc && hdc_src) {
 | 
						|
      SetStretchBltMode(hdc, HALFTONE);
 | 
						|
 | 
						|
      // resize color channels
 | 
						|
      SelectObject(hdc, color_bitmap);
 | 
						|
      SelectObject(hdc_src, bitmap);
 | 
						|
      StretchBlt(hdc, 0, 0, width, height, hdc_src, 0, 0, bm.bmWidth,
 | 
						|
                 bm.bmHeight, SRCCOPY);
 | 
						|
 | 
						|
      // resize alpha channel
 | 
						|
      SelectObject(hdc, alpha_bitmap);
 | 
						|
      SelectObject(hdc_src, alpha_src_bitmap);
 | 
						|
      StretchBlt(hdc, 0, 0, width, height, hdc_src, 0, 0, bm.bmWidth,
 | 
						|
                 bm.bmHeight, SRCCOPY);
 | 
						|
 | 
						|
      // flush before touching the bits
 | 
						|
      GdiFlush();
 | 
						|
 | 
						|
      // apply the alpha channel
 | 
						|
      auto* dest = reinterpret_cast<BYTE*>(color_bits);
 | 
						|
      auto* src = reinterpret_cast<const BYTE*>(alpha_bits);
 | 
						|
      auto* end = src + (width * height * 4);
 | 
						|
      while (src != end) {
 | 
						|
        dest[3] = src[0];
 | 
						|
        dest += 4;
 | 
						|
        src += 4;
 | 
						|
      }
 | 
						|
 | 
						|
      // create the resulting bitmap
 | 
						|
      result_bitmap =
 | 
						|
          CreateDIBitmap(hdc_screen, &bmi, CBM_INIT, color_bits,
 | 
						|
                         reinterpret_cast<BITMAPINFO*>(&bmi), DIB_RGB_COLORS);
 | 
						|
    }
 | 
						|
 | 
						|
    if (hdc_src)
 | 
						|
      DeleteDC(hdc_src);
 | 
						|
    if (hdc)
 | 
						|
      DeleteDC(hdc);
 | 
						|
 | 
						|
    if (alpha_bitmap)
 | 
						|
      DeleteObject(alpha_bitmap);
 | 
						|
    if (color_bitmap)
 | 
						|
      DeleteObject(color_bitmap);
 | 
						|
 | 
						|
    DeleteObject(alpha_src_bitmap);
 | 
						|
  }
 | 
						|
 | 
						|
  ReleaseDC(NULL, hdc_screen);
 | 
						|
 | 
						|
  return result_bitmap;
 | 
						|
}
 | 
						|
 | 
						|
const TCHAR DesktopNotificationController::Toast::class_name_[] =
 | 
						|
    TEXT("DesktopNotificationToast");
 | 
						|
 | 
						|
DesktopNotificationController::Toast::Toast(HWND hwnd,
 | 
						|
                                            shared_ptr<NotificationData>* data)
 | 
						|
    : hwnd_(hwnd), data_(*data) {
 | 
						|
  HDC hdc_screen = GetDC(NULL);
 | 
						|
  hdc_ = CreateCompatibleDC(hdc_screen);
 | 
						|
  ReleaseDC(NULL, hdc_screen);
 | 
						|
}
 | 
						|
 | 
						|
DesktopNotificationController::Toast::~Toast() {
 | 
						|
  if (uia_) {
 | 
						|
    auto* UiaDisconnectProvider =
 | 
						|
        reinterpret_cast<decltype(&::UiaDisconnectProvider)>(GetProcAddress(
 | 
						|
            GetModuleHandle(L"uiautomationcore.dll"), "UiaDisconnectProvider"));
 | 
						|
    // first detach from the toast, then call UiaDisconnectProvider;
 | 
						|
    // UiaDisconnectProvider may call WM_GETOBJECT and we don't want
 | 
						|
    // it to return the object that we're disconnecting
 | 
						|
    uia_->DetachToast();
 | 
						|
 | 
						|
    if (UiaDisconnectProvider)
 | 
						|
      UiaDisconnectProvider(uia_);
 | 
						|
 | 
						|
    uia_->Release();
 | 
						|
    uia_ = nullptr;
 | 
						|
  }
 | 
						|
 | 
						|
  DeleteDC(hdc_);
 | 
						|
  if (bitmap_)
 | 
						|
    DeleteBitmap(bitmap_);
 | 
						|
  if (scaled_image_)
 | 
						|
    DeleteBitmap(scaled_image_);
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::Register(HINSTANCE hinstance) {
 | 
						|
  WNDCLASSEX wc = {sizeof(wc)};
 | 
						|
  wc.lpfnWndProc = &Toast::WndProc;
 | 
						|
  wc.lpszClassName = class_name_;
 | 
						|
  wc.cbWndExtra = sizeof(Toast*);
 | 
						|
  wc.hInstance = hinstance;
 | 
						|
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
 | 
						|
 | 
						|
  RegisterClassEx(&wc);
 | 
						|
}
 | 
						|
 | 
						|
LRESULT DesktopNotificationController::Toast::WndProc(HWND hwnd,
 | 
						|
                                                      UINT message,
 | 
						|
                                                      WPARAM wparam,
 | 
						|
                                                      LPARAM lparam) {
 | 
						|
  switch (message) {
 | 
						|
    case WM_CREATE: {
 | 
						|
      auto*& cs = reinterpret_cast<const CREATESTRUCT*&>(lparam);
 | 
						|
      auto* data =
 | 
						|
          static_cast<shared_ptr<NotificationData>*>(cs->lpCreateParams);
 | 
						|
      auto* inst = new Toast(hwnd, data);
 | 
						|
      SetWindowLongPtr(hwnd, 0, (LONG_PTR)inst);
 | 
						|
    } break;
 | 
						|
 | 
						|
    case WM_NCDESTROY:
 | 
						|
      delete Get(hwnd);
 | 
						|
      SetWindowLongPtr(hwnd, 0, 0);
 | 
						|
      return 0;
 | 
						|
 | 
						|
    case WM_DESTROY:
 | 
						|
      if (Get(hwnd)->uia_) {
 | 
						|
        // free UI Automation resources associated with this window
 | 
						|
        UiaReturnRawElementProvider(hwnd, 0, 0, nullptr);
 | 
						|
      }
 | 
						|
      break;
 | 
						|
 | 
						|
    case WM_MOUSEACTIVATE:
 | 
						|
      return MA_NOACTIVATE;
 | 
						|
 | 
						|
    case WM_TIMER:
 | 
						|
      if (wparam == TimerID_AutoDismiss) {
 | 
						|
        Get(hwnd)->AutoDismiss();
 | 
						|
      }
 | 
						|
      return 0;
 | 
						|
 | 
						|
    case WM_LBUTTONDOWN: {
 | 
						|
      auto* inst = Get(hwnd);
 | 
						|
 | 
						|
      inst->Dismiss();
 | 
						|
 | 
						|
      Notification notification(inst->data_);
 | 
						|
      if (inst->is_close_hot_)
 | 
						|
        inst->data_->controller->OnNotificationDismissed(notification);
 | 
						|
      else
 | 
						|
        inst->data_->controller->OnNotificationClicked(notification);
 | 
						|
    }
 | 
						|
      return 0;
 | 
						|
 | 
						|
    case WM_MOUSEMOVE: {
 | 
						|
      auto* inst = Get(hwnd);
 | 
						|
      if (!inst->is_highlighted_) {
 | 
						|
        inst->is_highlighted_ = true;
 | 
						|
 | 
						|
        TRACKMOUSEEVENT tme = {sizeof(tme), TME_LEAVE, hwnd};
 | 
						|
        TrackMouseEvent(&tme);
 | 
						|
      }
 | 
						|
 | 
						|
      POINT cursor = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
 | 
						|
      inst->is_close_hot_ =
 | 
						|
          (PtInRect(&inst->close_button_rect_, cursor) != FALSE);
 | 
						|
 | 
						|
      if (!inst->is_non_interactive_)
 | 
						|
        inst->CancelDismiss();
 | 
						|
 | 
						|
      inst->UpdateContents();
 | 
						|
    }
 | 
						|
      return 0;
 | 
						|
 | 
						|
    case WM_MOUSELEAVE: {
 | 
						|
      auto* inst = Get(hwnd);
 | 
						|
      inst->is_highlighted_ = false;
 | 
						|
      inst->is_close_hot_ = false;
 | 
						|
      inst->UpdateContents();
 | 
						|
 | 
						|
      if (!inst->ease_out_active_ && inst->ease_in_pos_ == 1.0f)
 | 
						|
        inst->ScheduleDismissal();
 | 
						|
 | 
						|
      // Make sure stack collapse happens if needed
 | 
						|
      inst->data_->controller->StartAnimation();
 | 
						|
    }
 | 
						|
      return 0;
 | 
						|
 | 
						|
    case WM_WINDOWPOSCHANGED: {
 | 
						|
      auto*& wp = reinterpret_cast<WINDOWPOS*&>(lparam);
 | 
						|
      if (wp->flags & SWP_HIDEWINDOW) {
 | 
						|
        if (!IsWindowVisible(hwnd))
 | 
						|
          Get(hwnd)->is_highlighted_ = false;
 | 
						|
      }
 | 
						|
    } break;
 | 
						|
 | 
						|
    case WM_GETOBJECT:
 | 
						|
      if (lparam == UiaRootObjectId) {
 | 
						|
        auto* inst = Get(hwnd);
 | 
						|
        if (!inst->uia_) {
 | 
						|
          inst->uia_ = new UIAutomationInterface(inst);
 | 
						|
          inst->uia_->AddRef();
 | 
						|
        }
 | 
						|
        // don't return the interface if it's being disconnected
 | 
						|
        if (!inst->uia_->IsDetached()) {
 | 
						|
          return UiaReturnRawElementProvider(hwnd, wparam, lparam, inst->uia_);
 | 
						|
        }
 | 
						|
      }
 | 
						|
      break;
 | 
						|
  }
 | 
						|
 | 
						|
  return DefWindowProc(hwnd, message, wparam, lparam);
 | 
						|
}
 | 
						|
 | 
						|
HWND DesktopNotificationController::Toast::Create(
 | 
						|
    HINSTANCE hinstance,
 | 
						|
    shared_ptr<NotificationData> data) {
 | 
						|
  return CreateWindowEx(WS_EX_LAYERED | WS_EX_NOACTIVATE | WS_EX_TOPMOST,
 | 
						|
                        class_name_, nullptr, WS_POPUP, 0, 0, 0, 0, NULL, NULL,
 | 
						|
                        hinstance, &data);
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::Draw() {
 | 
						|
  const COLORREF accent = GetAccentColor();
 | 
						|
 | 
						|
  COLORREF back_color;
 | 
						|
  {
 | 
						|
    // base background color is 2/3 of accent
 | 
						|
    // highlighted adds a bit of intensity to every channel
 | 
						|
 | 
						|
    int h = is_highlighted_ ? (0xff / 20) : 0;
 | 
						|
 | 
						|
    back_color = RGB(min(0xff, (GetRValue(accent) * 2 / 3) + h),
 | 
						|
                     min(0xff, (GetGValue(accent) * 2 / 3) + h),
 | 
						|
                     min(0xff, (GetBValue(accent) * 2 / 3) + h));
 | 
						|
  }
 | 
						|
 | 
						|
  const float back_luma = (GetRValue(back_color) * 0.299f / 255) +
 | 
						|
                          (GetGValue(back_color) * 0.587f / 255) +
 | 
						|
                          (GetBValue(back_color) * 0.114f / 255);
 | 
						|
 | 
						|
  const struct {
 | 
						|
    float r, g, b;
 | 
						|
  } back_f = {
 | 
						|
      GetRValue(back_color) / 255.0f,
 | 
						|
      GetGValue(back_color) / 255.0f,
 | 
						|
      GetBValue(back_color) / 255.0f,
 | 
						|
  };
 | 
						|
 | 
						|
  COLORREF fore_color, dimmed_color;
 | 
						|
  {
 | 
						|
    // based on the lightness of background, we draw foreground in light
 | 
						|
    // or dark shades of gray blended onto the background with slight
 | 
						|
    // transparency to avoid sharp contrast
 | 
						|
 | 
						|
    constexpr float alpha = 0.9f;
 | 
						|
    constexpr float intensity_light[] = {(1.0f * alpha), (0.8f * alpha)};
 | 
						|
    constexpr float intensity_dark[] = {(0.1f * alpha), (0.3f * alpha)};
 | 
						|
 | 
						|
    // select foreground intensity values (light or dark)
 | 
						|
    auto& i = (back_luma < 0.6f) ? intensity_light : intensity_dark;
 | 
						|
 | 
						|
    float r, g, b;
 | 
						|
 | 
						|
    r = i[0] + back_f.r * (1 - alpha);
 | 
						|
    g = i[0] + back_f.g * (1 - alpha);
 | 
						|
    b = i[0] + back_f.b * (1 - alpha);
 | 
						|
    fore_color = RGB(r * 0xff, g * 0xff, b * 0xff);
 | 
						|
 | 
						|
    r = i[1] + back_f.r * (1 - alpha);
 | 
						|
    g = i[1] + back_f.g * (1 - alpha);
 | 
						|
    b = i[1] + back_f.b * (1 - alpha);
 | 
						|
    dimmed_color = RGB(r * 0xff, g * 0xff, b * 0xff);
 | 
						|
  }
 | 
						|
 | 
						|
  // Draw background
 | 
						|
  {
 | 
						|
    auto* brush = CreateSolidBrush(back_color);
 | 
						|
 | 
						|
    RECT rc = {0, 0, toast_size_.cx, toast_size_.cy};
 | 
						|
    FillRect(hdc_, &rc, brush);
 | 
						|
 | 
						|
    DeleteBrush(brush);
 | 
						|
  }
 | 
						|
 | 
						|
  SetBkMode(hdc_, TRANSPARENT);
 | 
						|
 | 
						|
  const auto close = L'\x2715';
 | 
						|
  auto* caption_font = data_->controller->GetCaptionFont();
 | 
						|
  auto* body_font = data_->controller->GetBodyFont();
 | 
						|
 | 
						|
  TEXTMETRIC tm_cap;
 | 
						|
  SelectFont(hdc_, caption_font);
 | 
						|
  GetTextMetrics(hdc_, &tm_cap);
 | 
						|
 | 
						|
  auto text_offset_x = margin_.cx;
 | 
						|
 | 
						|
  BITMAP image_info = {};
 | 
						|
  if (scaled_image_) {
 | 
						|
    GetObject(scaled_image_, sizeof(image_info), &image_info);
 | 
						|
 | 
						|
    text_offset_x += margin_.cx + image_info.bmWidth;
 | 
						|
  }
 | 
						|
 | 
						|
  // calculate close button rect
 | 
						|
  POINT close_pos;
 | 
						|
  {
 | 
						|
    SIZE extent = {};
 | 
						|
    GetTextExtentPoint32W(hdc_, &close, 1, &extent);
 | 
						|
 | 
						|
    close_button_rect_.right = toast_size_.cx;
 | 
						|
    close_button_rect_.top = 0;
 | 
						|
 | 
						|
    close_pos.x = close_button_rect_.right - margin_.cy - extent.cx;
 | 
						|
    close_pos.y = close_button_rect_.top + margin_.cy;
 | 
						|
 | 
						|
    close_button_rect_.left = close_pos.x - margin_.cy;
 | 
						|
    close_button_rect_.bottom = close_pos.y + extent.cy + margin_.cy;
 | 
						|
  }
 | 
						|
 | 
						|
  // image
 | 
						|
  if (scaled_image_) {
 | 
						|
    HDC hdc_image = CreateCompatibleDC(NULL);
 | 
						|
    SelectBitmap(hdc_image, scaled_image_);
 | 
						|
    BLENDFUNCTION blend = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
 | 
						|
    AlphaBlend(hdc_, margin_.cx, margin_.cy, image_info.bmWidth,
 | 
						|
               image_info.bmHeight, hdc_image, 0, 0, image_info.bmWidth,
 | 
						|
               image_info.bmHeight, blend);
 | 
						|
    DeleteDC(hdc_image);
 | 
						|
  }
 | 
						|
 | 
						|
  // caption
 | 
						|
  {
 | 
						|
    RECT rc = {text_offset_x, margin_.cy, close_button_rect_.left,
 | 
						|
               toast_size_.cy};
 | 
						|
 | 
						|
    SelectFont(hdc_, caption_font);
 | 
						|
    SetTextColor(hdc_, fore_color);
 | 
						|
    DrawText(hdc_, data_->caption.data(), (UINT)data_->caption.length(), &rc,
 | 
						|
             DT_SINGLELINE | DT_END_ELLIPSIS | DT_NOPREFIX);
 | 
						|
  }
 | 
						|
 | 
						|
  // body text
 | 
						|
  if (!data_->body_text.empty()) {
 | 
						|
    RECT rc = {text_offset_x, 2 * margin_.cy + tm_cap.tmAscent,
 | 
						|
               toast_size_.cx - margin_.cx, toast_size_.cy - margin_.cy};
 | 
						|
 | 
						|
    SelectFont(hdc_, body_font);
 | 
						|
    SetTextColor(hdc_, dimmed_color);
 | 
						|
    DrawText(hdc_, data_->body_text.data(), (UINT)data_->body_text.length(),
 | 
						|
             &rc,
 | 
						|
             DT_LEFT | DT_WORDBREAK | DT_NOPREFIX | DT_END_ELLIPSIS |
 | 
						|
                 DT_EDITCONTROL);
 | 
						|
  }
 | 
						|
 | 
						|
  // close button
 | 
						|
  {
 | 
						|
    SelectFont(hdc_, caption_font);
 | 
						|
    SetTextColor(hdc_, is_close_hot_ ? fore_color : dimmed_color);
 | 
						|
    ExtTextOut(hdc_, close_pos.x, close_pos.y, 0, nullptr, &close, 1, nullptr);
 | 
						|
  }
 | 
						|
 | 
						|
  is_content_updated_ = true;
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::Invalidate() {
 | 
						|
  is_content_updated_ = false;
 | 
						|
}
 | 
						|
 | 
						|
bool DesktopNotificationController::Toast::IsRedrawNeeded() const {
 | 
						|
  return !is_content_updated_;
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::UpdateBufferSize() {
 | 
						|
  if (hdc_) {
 | 
						|
    SIZE new_size;
 | 
						|
    {
 | 
						|
      TEXTMETRIC tm_cap = {};
 | 
						|
      HFONT font = data_->controller->GetCaptionFont();
 | 
						|
      if (font) {
 | 
						|
        SelectFont(hdc_, font);
 | 
						|
        if (!GetTextMetrics(hdc_, &tm_cap))
 | 
						|
          return;
 | 
						|
      }
 | 
						|
 | 
						|
      TEXTMETRIC tm_body = {};
 | 
						|
      font = data_->controller->GetBodyFont();
 | 
						|
      if (font) {
 | 
						|
        SelectFont(hdc_, font);
 | 
						|
        if (!GetTextMetrics(hdc_, &tm_body))
 | 
						|
          return;
 | 
						|
      }
 | 
						|
 | 
						|
      this->margin_ = {tm_cap.tmAveCharWidth * 2, tm_cap.tmAscent / 2};
 | 
						|
 | 
						|
      new_size.cx = margin_.cx + (32 * tm_cap.tmAveCharWidth) + margin_.cx;
 | 
						|
      new_size.cy = margin_.cy + (tm_cap.tmHeight) + margin_.cy;
 | 
						|
 | 
						|
      if (!data_->body_text.empty())
 | 
						|
        new_size.cy += margin_.cy + (3 * tm_body.tmHeight);
 | 
						|
 | 
						|
      if (data_->image) {
 | 
						|
        BITMAP bm;
 | 
						|
        if (GetObject(data_->image, sizeof(bm), &bm)) {
 | 
						|
          // cap the image size
 | 
						|
          const int max_dim_size = 80;
 | 
						|
 | 
						|
          auto width = bm.bmWidth;
 | 
						|
          auto height = bm.bmHeight;
 | 
						|
          if (width < height) {
 | 
						|
            if (height > max_dim_size) {
 | 
						|
              width = width * max_dim_size / height;
 | 
						|
              height = max_dim_size;
 | 
						|
            }
 | 
						|
          } else {
 | 
						|
            if (width > max_dim_size) {
 | 
						|
              height = height * max_dim_size / width;
 | 
						|
              width = max_dim_size;
 | 
						|
            }
 | 
						|
          }
 | 
						|
 | 
						|
          ScreenMetrics scr;
 | 
						|
          SIZE image_draw_size = {scr.X(width), scr.Y(height)};
 | 
						|
 | 
						|
          new_size.cx += image_draw_size.cx + margin_.cx;
 | 
						|
 | 
						|
          auto height_with_image =
 | 
						|
              margin_.cy + (image_draw_size.cy) + margin_.cy;
 | 
						|
 | 
						|
          if (new_size.cy < height_with_image)
 | 
						|
            new_size.cy = height_with_image;
 | 
						|
 | 
						|
          UpdateScaledImage(image_draw_size);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
    if (new_size.cx != this->toast_size_.cx ||
 | 
						|
        new_size.cy != this->toast_size_.cy) {
 | 
						|
      HDC hdc_screen = GetDC(NULL);
 | 
						|
      auto* new_bitmap =
 | 
						|
          CreateCompatibleBitmap(hdc_screen, new_size.cx, new_size.cy);
 | 
						|
      ReleaseDC(NULL, hdc_screen);
 | 
						|
 | 
						|
      if (new_bitmap) {
 | 
						|
        if (SelectBitmap(hdc_, new_bitmap)) {
 | 
						|
          RECT dirty1 = {}, dirty2 = {};
 | 
						|
          if (toast_size_.cx < new_size.cx) {
 | 
						|
            dirty1 = {toast_size_.cx, 0, new_size.cx, toast_size_.cy};
 | 
						|
          }
 | 
						|
          if (toast_size_.cy < new_size.cy) {
 | 
						|
            dirty2 = {0, toast_size_.cy, new_size.cx, new_size.cy};
 | 
						|
          }
 | 
						|
 | 
						|
          if (this->bitmap_)
 | 
						|
            DeleteBitmap(this->bitmap_);
 | 
						|
          this->bitmap_ = new_bitmap;
 | 
						|
          this->toast_size_ = new_size;
 | 
						|
 | 
						|
          Invalidate();
 | 
						|
 | 
						|
          // Resize also the DWM buffer to prevent flicker during
 | 
						|
          // window resizing. Make sure any existing data is not
 | 
						|
          // overwritten by marking the dirty region.
 | 
						|
          {
 | 
						|
            POINT origin = {0, 0};
 | 
						|
 | 
						|
            UPDATELAYEREDWINDOWINFO ulw;
 | 
						|
            ulw.cbSize = sizeof(ulw);
 | 
						|
            ulw.hdcDst = NULL;
 | 
						|
            ulw.pptDst = nullptr;
 | 
						|
            ulw.psize = &toast_size_;
 | 
						|
            ulw.hdcSrc = hdc_;
 | 
						|
            ulw.pptSrc = &origin;
 | 
						|
            ulw.crKey = 0;
 | 
						|
            ulw.pblend = nullptr;
 | 
						|
            ulw.dwFlags = 0;
 | 
						|
            ulw.prcDirty = &dirty1;
 | 
						|
            auto b1 = UpdateLayeredWindowIndirect(hwnd_, &ulw);
 | 
						|
            ulw.prcDirty = &dirty2;
 | 
						|
            auto b2 = UpdateLayeredWindowIndirect(hwnd_, &ulw);
 | 
						|
            DCHECK(b1 && b2);
 | 
						|
          }
 | 
						|
 | 
						|
          return;
 | 
						|
        }
 | 
						|
 | 
						|
        DeleteBitmap(new_bitmap);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::UpdateScaledImage(const SIZE& size) {
 | 
						|
  BITMAP bm;
 | 
						|
  if (!GetObject(scaled_image_, sizeof(bm), &bm) || bm.bmWidth != size.cx ||
 | 
						|
      bm.bmHeight != size.cy) {
 | 
						|
    if (scaled_image_)
 | 
						|
      DeleteBitmap(scaled_image_);
 | 
						|
    scaled_image_ = StretchBitmap(data_->image, size.cx, size.cy);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::UpdateContents() {
 | 
						|
  Draw();
 | 
						|
 | 
						|
  if (IsWindowVisible(hwnd_)) {
 | 
						|
    RECT rc;
 | 
						|
    GetWindowRect(hwnd_, &rc);
 | 
						|
    POINT origin = {0, 0};
 | 
						|
    SIZE size = {rc.right - rc.left, rc.bottom - rc.top};
 | 
						|
    UpdateLayeredWindow(hwnd_, NULL, nullptr, &size, hdc_, &origin, 0, nullptr,
 | 
						|
                        0);
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::Dismiss() {
 | 
						|
  if (!is_non_interactive_) {
 | 
						|
    // Set a flag to prevent further interaction. We don't disable the HWND
 | 
						|
    // because we still want to receive mouse move messages in order to keep
 | 
						|
    // the toast under the cursor and not collapse it while dismissing.
 | 
						|
    is_non_interactive_ = true;
 | 
						|
 | 
						|
    AutoDismiss();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::AutoDismiss() {
 | 
						|
  KillTimer(hwnd_, TimerID_AutoDismiss);
 | 
						|
  StartEaseOut();
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::CancelDismiss() {
 | 
						|
  KillTimer(hwnd_, TimerID_AutoDismiss);
 | 
						|
  ease_out_active_ = false;
 | 
						|
  ease_out_pos_ = 0;
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::ScheduleDismissal() {
 | 
						|
  ULONG duration;
 | 
						|
  if (!SystemParametersInfo(SPI_GETMESSAGEDURATION, 0, &duration, 0)) {
 | 
						|
    duration = 5;
 | 
						|
  }
 | 
						|
  SetTimer(hwnd_, TimerID_AutoDismiss, duration * 1000, nullptr);
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::ResetContents() {
 | 
						|
  if (scaled_image_) {
 | 
						|
    DeleteBitmap(scaled_image_);
 | 
						|
    scaled_image_ = NULL;
 | 
						|
  }
 | 
						|
 | 
						|
  Invalidate();
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::PopUp(int y) {
 | 
						|
  vertical_pos_target_ = vertical_pos_ = y;
 | 
						|
  StartEaseIn();
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::SetVerticalPosition(int y) {
 | 
						|
  // Don't restart animation if current target is the same
 | 
						|
  if (y == vertical_pos_target_)
 | 
						|
    return;
 | 
						|
 | 
						|
  // Make sure the new animation's origin is at the current position
 | 
						|
  vertical_pos_ += static_cast<int>((vertical_pos_target_ - vertical_pos_) *
 | 
						|
                                    stack_collapse_pos_);
 | 
						|
 | 
						|
  // Set new target position and start the animation
 | 
						|
  vertical_pos_target_ = y;
 | 
						|
  stack_collapse_start_ = GetTickCount();
 | 
						|
  data_->controller->StartAnimation();
 | 
						|
}
 | 
						|
 | 
						|
HDWP DesktopNotificationController::Toast::Animate(HDWP hdwp,
 | 
						|
                                                   const POINT& origin) {
 | 
						|
  UpdateBufferSize();
 | 
						|
 | 
						|
  if (IsRedrawNeeded())
 | 
						|
    Draw();
 | 
						|
 | 
						|
  POINT src_origin = {0, 0};
 | 
						|
 | 
						|
  UPDATELAYEREDWINDOWINFO ulw;
 | 
						|
  ulw.cbSize = sizeof(ulw);
 | 
						|
  ulw.hdcDst = NULL;
 | 
						|
  ulw.pptDst = nullptr;
 | 
						|
  ulw.psize = nullptr;
 | 
						|
  ulw.hdcSrc = hdc_;
 | 
						|
  ulw.pptSrc = &src_origin;
 | 
						|
  ulw.crKey = 0;
 | 
						|
  ulw.pblend = nullptr;
 | 
						|
  ulw.dwFlags = 0;
 | 
						|
  ulw.prcDirty = nullptr;
 | 
						|
 | 
						|
  POINT pt = {0, 0};
 | 
						|
  SIZE size = {0, 0};
 | 
						|
  BLENDFUNCTION blend;
 | 
						|
  UINT dwpFlags =
 | 
						|
      SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOREDRAW | SWP_NOCOPYBITS;
 | 
						|
 | 
						|
  auto ease_in_pos = AnimateEaseIn();
 | 
						|
  auto ease_out_pos = AnimateEaseOut();
 | 
						|
  auto stack_collapse_pos = AnimateStackCollapse();
 | 
						|
 | 
						|
  auto y_offset = (vertical_pos_target_ - vertical_pos_) * stack_collapse_pos;
 | 
						|
 | 
						|
  size.cx = static_cast<int>(toast_size_.cx * ease_in_pos);
 | 
						|
  size.cy = toast_size_.cy;
 | 
						|
 | 
						|
  pt.x = origin.x - size.cx;
 | 
						|
  pt.y = static_cast<int>(origin.y - vertical_pos_ - y_offset - size.cy);
 | 
						|
 | 
						|
  ulw.pptDst = &pt;
 | 
						|
  ulw.psize = &size;
 | 
						|
 | 
						|
  if (ease_in_active_ && ease_in_pos == 1.0f) {
 | 
						|
    ease_in_active_ = false;
 | 
						|
    ScheduleDismissal();
 | 
						|
  }
 | 
						|
 | 
						|
  this->ease_in_pos_ = ease_in_pos;
 | 
						|
  this->stack_collapse_pos_ = stack_collapse_pos;
 | 
						|
 | 
						|
  if (ease_out_pos != this->ease_out_pos_) {
 | 
						|
    blend.BlendOp = AC_SRC_OVER;
 | 
						|
    blend.BlendFlags = 0;
 | 
						|
    blend.SourceConstantAlpha = (BYTE)(255 * (1.0f - ease_out_pos));
 | 
						|
    blend.AlphaFormat = 0;
 | 
						|
 | 
						|
    ulw.pblend = &blend;
 | 
						|
    ulw.dwFlags = ULW_ALPHA;
 | 
						|
 | 
						|
    this->ease_out_pos_ = ease_out_pos;
 | 
						|
 | 
						|
    if (ease_out_pos == 1.0f) {
 | 
						|
      ease_out_active_ = false;
 | 
						|
 | 
						|
      dwpFlags &= ~SWP_SHOWWINDOW;
 | 
						|
      dwpFlags |= SWP_HIDEWINDOW;
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  if (stack_collapse_pos == 1.0f) {
 | 
						|
    vertical_pos_ = vertical_pos_target_;
 | 
						|
  }
 | 
						|
 | 
						|
  // `UpdateLayeredWindowIndirect` updates position, size, and transparency.
 | 
						|
  // `DeferWindowPos` updates z-order, and also position and size in case
 | 
						|
  // ULWI fails, which can happen when one of the dimensions is zero (e.g.
 | 
						|
  // at the beginning of ease-in).
 | 
						|
 | 
						|
  UpdateLayeredWindowIndirect(hwnd_, &ulw);
 | 
						|
  hdwp = DeferWindowPos(hdwp, hwnd_, HWND_TOPMOST, pt.x, pt.y, size.cx, size.cy,
 | 
						|
                        dwpFlags);
 | 
						|
  return hdwp;
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::StartEaseIn() {
 | 
						|
  DCHECK(!ease_in_active_);
 | 
						|
  ease_in_start_ = GetTickCount();
 | 
						|
  ease_in_active_ = true;
 | 
						|
  data_->controller->StartAnimation();
 | 
						|
}
 | 
						|
 | 
						|
void DesktopNotificationController::Toast::StartEaseOut() {
 | 
						|
  DCHECK(!ease_out_active_);
 | 
						|
  ease_out_start_ = GetTickCount();
 | 
						|
  ease_out_active_ = true;
 | 
						|
  data_->controller->StartAnimation();
 | 
						|
}
 | 
						|
 | 
						|
bool DesktopNotificationController::Toast::IsStackCollapseActive() const {
 | 
						|
  return (vertical_pos_ != vertical_pos_target_);
 | 
						|
}
 | 
						|
 | 
						|
float DesktopNotificationController::Toast::AnimateEaseIn() {
 | 
						|
  if (!ease_in_active_)
 | 
						|
    return ease_in_pos_;
 | 
						|
 | 
						|
  constexpr DWORD duration = 500;
 | 
						|
  auto elapsed = GetTickCount() - ease_in_start_;
 | 
						|
  float time = std::min(duration, elapsed) / static_cast<float>(duration);
 | 
						|
 | 
						|
  // decelerating exponential ease
 | 
						|
  const float a = -8.0f;
 | 
						|
  auto pos = (std::exp(a * time) - 1.0f) / (std::exp(a) - 1.0f);
 | 
						|
 | 
						|
  return pos;
 | 
						|
}
 | 
						|
 | 
						|
float DesktopNotificationController::Toast::AnimateEaseOut() {
 | 
						|
  if (!ease_out_active_)
 | 
						|
    return ease_out_pos_;
 | 
						|
 | 
						|
  constexpr DWORD duration = 120;
 | 
						|
  auto elapsed = GetTickCount() - ease_out_start_;
 | 
						|
  float time = std::min(duration, elapsed) / static_cast<float>(duration);
 | 
						|
 | 
						|
  // accelerating circle ease
 | 
						|
  auto pos = 1.0f - std::sqrt(1 - time * time);
 | 
						|
 | 
						|
  return pos;
 | 
						|
}
 | 
						|
 | 
						|
float DesktopNotificationController::Toast::AnimateStackCollapse() {
 | 
						|
  if (!IsStackCollapseActive())
 | 
						|
    return stack_collapse_pos_;
 | 
						|
 | 
						|
  constexpr DWORD duration = 500;
 | 
						|
  auto elapsed = GetTickCount() - stack_collapse_start_;
 | 
						|
  float time = std::min(duration, elapsed) / static_cast<float>(duration);
 | 
						|
 | 
						|
  // decelerating exponential ease
 | 
						|
  const float a = -8.0f;
 | 
						|
  auto pos = (std::exp(a * time) - 1.0f) / (std::exp(a) - 1.0f);
 | 
						|
 | 
						|
  return pos;
 | 
						|
}
 | 
						|
 | 
						|
}  // namespace atom
 |