electron/atom/browser/notifications/win/win32_desktop_notifications/toast.cc

859 lines
24 KiB
C++
Raw Normal View History

// 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 <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() {
2018-04-18 01:56:12 +00:00
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;
}
2018-04-18 01:56:12 +00:00
}
2018-04-18 01:56:12 +00:00
return GetSysColor(COLOR_ACTIVECAPTION);
}
// Stretches a bitmap to the specified size, preserves alpha channel
static HBITMAP StretchBitmap(HBITMAP bitmap, unsigned width, unsigned height) {
2018-04-18 01:56:12 +00:00
// 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.
2018-04-18 01:56:12 +00:00
BITMAP bm;
if (!GetObject(bitmap, sizeof(bm), &bm))
return NULL;
2018-04-18 01:56:12 +00:00
if (width == 0 || height == 0)
return NULL;
2018-04-18 01:56:12 +00:00
HBITMAP result_bitmap = NULL;
2018-04-18 01:56:12 +00:00
HDC hdc_screen = GetDC(NULL);
2018-04-18 01:56:12 +00:00
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) {
2018-04-18 01:56:12 +00:00
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*>(
2018-04-18 01:56:12 +00:00
_aligned_malloc(bmi.biSizeImage, sizeof(DWORD)));
2018-04-18 01:56:12 +00:00
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);
2018-04-18 01:56:12 +00:00
BYTE* dest = reinterpret_cast<BYTE*>(alpha_src_bits);
2018-04-18 01:56:12 +00:00
for (; src != end; ++src, ++dest) {
BYTE a = *src >> 24;
*dest++ = a;
*dest++ = a;
*dest++ = a;
}
2018-04-18 01:56:12 +00:00
_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 =
2018-04-18 01:56:12 +00:00
CreateDIBSection(NULL, reinterpret_cast<BITMAPINFO*>(&bmi),
DIB_RGB_COLORS, &color_bits, NULL, 0);
void* alpha_bits;
auto* alpha_bitmap =
2018-04-18 01:56:12 +00:00
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);
2018-04-18 01:56:12 +00:00
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);
}
2018-04-18 01:56:12 +00:00
if (hdc_src)
DeleteDC(hdc_src);
if (hdc)
DeleteDC(hdc);
if (alpha_bitmap)
DeleteObject(alpha_bitmap);
if (color_bitmap)
DeleteObject(color_bitmap);
2018-04-18 01:56:12 +00:00
DeleteObject(alpha_src_bitmap);
}
ReleaseDC(NULL, hdc_screen);
return result_bitmap;
}
const TCHAR DesktopNotificationController::Toast::class_name_[] =
TEXT("DesktopNotificationToast");
2018-04-18 01:56:12 +00:00
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;
}
2018-04-18 01:56:12 +00:00
DeleteDC(hdc_);
if (bitmap_)
DeleteBitmap(bitmap_);
if (scaled_image_)
DeleteBitmap(scaled_image_);
}
void DesktopNotificationController::Toast::Register(HINSTANCE hinstance) {
2018-04-18 01:56:12 +00:00
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);
}
2018-04-18 01:56:12 +00:00
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 =
2018-04-18 01:56:12 +00:00
static_cast<shared_ptr<NotificationData>*>(cs->lpCreateParams);
auto* inst = new Toast(hwnd, data);
2018-04-18 01:56:12 +00:00
SetWindowLongPtr(hwnd, 0, (LONG_PTR)inst);
} break;
case WM_NCDESTROY:
2018-04-18 01:56:12 +00:00
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:
2018-04-18 01:56:12 +00:00
return MA_NOACTIVATE;
case WM_TIMER:
2018-04-18 01:56:12 +00:00
if (wparam == TimerID_AutoDismiss) {
Get(hwnd)->AutoDismiss();
}
return 0;
2018-04-18 01:56:12 +00:00
case WM_LBUTTONDOWN: {
auto* inst = Get(hwnd);
2018-04-18 01:56:12 +00:00
inst->Dismiss();
2018-04-18 01:56:12 +00:00
Notification notification(inst->data_);
if (inst->is_close_hot_)
inst->data_->controller->OnNotificationDismissed(notification);
else
inst->data_->controller->OnNotificationClicked(notification);
}
return 0;
2018-04-18 01:56:12 +00:00
case WM_MOUSEMOVE: {
auto* inst = Get(hwnd);
2018-04-18 01:56:12 +00:00
if (!inst->is_highlighted_) {
inst->is_highlighted_ = true;
2018-04-18 01:56:12 +00:00
TRACKMOUSEEVENT tme = {sizeof(tme), TME_LEAVE, hwnd};
TrackMouseEvent(&tme);
}
2018-04-18 01:56:12 +00:00
POINT cursor = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
inst->is_close_hot_ =
(PtInRect(&inst->close_button_rect_, cursor) != FALSE);
2018-04-18 01:56:12 +00:00
if (!inst->is_non_interactive_)
inst->CancelDismiss();
2018-04-18 01:56:12 +00:00
inst->UpdateContents();
}
return 0;
2018-04-18 01:56:12 +00:00
case WM_MOUSELEAVE: {
auto* inst = Get(hwnd);
2018-04-18 01:56:12 +00:00
inst->is_highlighted_ = false;
inst->is_close_hot_ = false;
inst->UpdateContents();
2018-04-18 01:56:12 +00:00
if (!inst->ease_out_active_ && inst->ease_in_pos_ == 1.0f)
inst->ScheduleDismissal();
2018-04-18 01:56:12 +00:00
// Make sure stack collapse happens if needed
inst->data_->controller->StartAnimation();
}
2018-04-18 01:56:12 +00:00
return 0;
case WM_WINDOWPOSCHANGED: {
auto*& wp = reinterpret_cast<WINDOWPOS*&>(lparam);
2018-04-18 01:56:12 +00:00
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;
2018-04-18 01:56:12 +00:00
}
return DefWindowProc(hwnd, message, wparam, lparam);
}
HWND DesktopNotificationController::Toast::Create(
2018-04-18 01:56:12 +00:00
HINSTANCE hinstance,
shared_ptr<NotificationData> data) {
2018-04-18 01:56:12 +00:00
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() {
2018-04-18 01:56:12 +00:00
const COLORREF accent = GetAccentColor();
2018-04-18 01:56:12 +00:00
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);
2018-04-18 01:56:12 +00:00
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();
2018-04-18 01:56:12 +00:00
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);
2018-04-18 01:56:12 +00:00
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);
}
2018-04-18 01:56:12 +00:00
is_content_updated_ = true;
}
void DesktopNotificationController::Toast::Invalidate() {
2018-04-18 01:56:12 +00:00
is_content_updated_ = false;
}
bool DesktopNotificationController::Toast::IsRedrawNeeded() const {
2018-04-18 01:56:12 +00:00
return !is_content_updated_;
}
void DesktopNotificationController::Toast::UpdateBufferSize() {
2018-04-18 01:56:12 +00:00
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;
}
2018-04-18 01:56:12 +00:00
} else {
if (width > max_dim_size) {
height = height * max_dim_size / width;
width = max_dim_size;
}
2018-04-18 01:56:12 +00:00
}
2018-04-18 01:56:12 +00:00
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);
}
2018-04-18 01:56:12 +00:00
}
}
2018-04-18 01:56:12 +00:00
if (new_size.cx != this->toast_size_.cx ||
new_size.cy != this->toast_size_.cy) {
HDC hdc_screen = GetDC(NULL);
auto* new_bitmap =
2018-04-18 01:56:12 +00:00
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);
2018-04-18 01:56:12 +00:00
}
return;
}
2018-04-18 01:56:12 +00:00
DeleteBitmap(new_bitmap);
}
}
2018-04-18 01:56:12 +00:00
}
}
void DesktopNotificationController::Toast::UpdateScaledImage(const SIZE& size) {
2018-04-18 01:56:12 +00:00
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() {
2018-04-18 01:56:12 +00:00
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() {
2018-04-18 01:56:12 +00:00
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() {
2018-04-18 01:56:12 +00:00
KillTimer(hwnd_, TimerID_AutoDismiss);
StartEaseOut();
}
void DesktopNotificationController::Toast::CancelDismiss() {
2018-04-18 01:56:12 +00:00
KillTimer(hwnd_, TimerID_AutoDismiss);
ease_out_active_ = false;
ease_out_pos_ = 0;
}
void DesktopNotificationController::Toast::ScheduleDismissal() {
2018-04-18 01:56:12 +00:00
ULONG duration;
if (!SystemParametersInfo(SPI_GETMESSAGEDURATION, 0, &duration, 0)) {
duration = 5;
}
SetTimer(hwnd_, TimerID_AutoDismiss, duration * 1000, nullptr);
}
void DesktopNotificationController::Toast::ResetContents() {
2018-04-18 01:56:12 +00:00
if (scaled_image_) {
DeleteBitmap(scaled_image_);
scaled_image_ = NULL;
}
2018-04-18 01:56:12 +00:00
Invalidate();
}
void DesktopNotificationController::Toast::PopUp(int y) {
2018-04-18 01:56:12 +00:00
vertical_pos_target_ = vertical_pos_ = y;
StartEaseIn();
}
void DesktopNotificationController::Toast::SetVerticalPosition(int y) {
2018-04-18 01:56:12 +00:00
// 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();
}
2018-04-18 01:56:12 +00:00
HDWP DesktopNotificationController::Toast::Animate(HDWP hdwp,
const POINT& origin) {
UpdateBufferSize();
2018-04-18 01:56:12 +00:00
if (IsRedrawNeeded())
Draw();
2018-04-18 01:56:12 +00:00
POINT src_origin = {0, 0};
2018-04-18 01:56:12 +00:00
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;
2018-04-18 01:56:12 +00:00
POINT pt = {0, 0};
SIZE size = {0, 0};
BLENDFUNCTION blend;
UINT dwpFlags =
SWP_NOACTIVATE | SWP_SHOWWINDOW | SWP_NOREDRAW | SWP_NOCOPYBITS;
2018-04-18 01:56:12 +00:00
auto ease_in_pos = AnimateEaseIn();
auto ease_out_pos = AnimateEaseOut();
auto stack_collapse_pos = AnimateStackCollapse();
2018-04-18 01:56:12 +00:00
auto y_offset = (vertical_pos_target_ - vertical_pos_) * stack_collapse_pos;
2018-04-18 01:56:12 +00:00
size.cx = static_cast<int>(toast_size_.cx * ease_in_pos);
size.cy = toast_size_.cy;
2018-04-18 01:56:12 +00:00
pt.x = origin.x - size.cx;
pt.y = static_cast<int>(origin.y - vertical_pos_ - y_offset - size.cy);
2018-04-18 01:56:12 +00:00
ulw.pptDst = &pt;
ulw.psize = &size;
2018-04-18 01:56:12 +00:00
if (ease_in_active_ && ease_in_pos == 1.0f) {
ease_in_active_ = false;
ScheduleDismissal();
}
2018-04-18 01:56:12 +00:00
this->ease_in_pos_ = ease_in_pos;
this->stack_collapse_pos_ = stack_collapse_pos;
2018-04-18 01:56:12 +00:00
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;
2018-04-18 01:56:12 +00:00
ulw.pblend = &blend;
ulw.dwFlags = ULW_ALPHA;
2018-04-18 01:56:12 +00:00
this->ease_out_pos_ = ease_out_pos;
2018-04-18 01:56:12 +00:00
if (ease_out_pos == 1.0f) {
ease_out_active_ = false;
2018-04-18 01:56:12 +00:00
dwpFlags &= ~SWP_SHOWWINDOW;
dwpFlags |= SWP_HIDEWINDOW;
}
2018-04-18 01:56:12 +00:00
}
2018-04-18 01:56:12 +00:00
if (stack_collapse_pos == 1.0f) {
vertical_pos_ = vertical_pos_target_;
}
2018-04-18 01:56:12 +00:00
// `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);
2018-04-18 01:56:12 +00:00
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_);
2018-04-18 01:56:12 +00:00
ease_in_start_ = GetTickCount();
ease_in_active_ = true;
data_->controller->StartAnimation();
}
void DesktopNotificationController::Toast::StartEaseOut() {
DCHECK(!ease_out_active_);
2018-04-18 01:56:12 +00:00
ease_out_start_ = GetTickCount();
ease_out_active_ = true;
data_->controller->StartAnimation();
}
bool DesktopNotificationController::Toast::IsStackCollapseActive() const {
2018-04-18 01:56:12 +00:00
return (vertical_pos_ != vertical_pos_target_);
}
float DesktopNotificationController::Toast::AnimateEaseIn() {
2018-04-18 01:56:12 +00:00
if (!ease_in_active_)
return ease_in_pos_;
2018-04-18 01:56:12 +00:00
constexpr DWORD duration = 500;
auto elapsed = GetTickCount() - ease_in_start_;
float time = std::min(duration, elapsed) / static_cast<float>(duration);
2018-04-18 01:56:12 +00:00
// decelerating exponential ease
const float a = -8.0f;
auto pos = (std::exp(a * time) - 1.0f) / (std::exp(a) - 1.0f);
2018-04-18 01:56:12 +00:00
return pos;
}
float DesktopNotificationController::Toast::AnimateEaseOut() {
2018-04-18 01:56:12 +00:00
if (!ease_out_active_)
return ease_out_pos_;
2018-04-18 01:56:12 +00:00
constexpr DWORD duration = 120;
auto elapsed = GetTickCount() - ease_out_start_;
float time = std::min(duration, elapsed) / static_cast<float>(duration);
2018-04-18 01:56:12 +00:00
// accelerating circle ease
auto pos = 1.0f - std::sqrt(1 - time * time);
2018-04-18 01:56:12 +00:00
return pos;
}
float DesktopNotificationController::Toast::AnimateStackCollapse() {
2018-04-18 01:56:12 +00:00
if (!IsStackCollapseActive())
return stack_collapse_pos_;
2018-04-18 01:56:12 +00:00
constexpr DWORD duration = 500;
auto elapsed = GetTickCount() - stack_collapse_start_;
float time = std::min(duration, elapsed) / static_cast<float>(duration);
2018-04-18 01:56:12 +00:00
// decelerating exponential ease
const float a = -8.0f;
auto pos = (std::exp(a * time) - 1.0f) / (std::exp(a) - 1.0f);
2018-04-18 01:56:12 +00:00
return pos;
}
} // namespace atom