diff --git a/brightray/browser/browser_client.cc b/brightray/browser/browser_client.cc index e425b7a83b1..7b25c6a7858 100644 --- a/brightray/browser/browser_client.cc +++ b/brightray/browser/browser_client.cc @@ -13,10 +13,6 @@ #include "browser/platform_notification_service.h" #include "content/public/common/url_constants.h" -#if defined(OS_WIN) -#include "base/win/windows_version.h" -#endif - namespace brightray { namespace { @@ -39,12 +35,6 @@ BrowserClient::~BrowserClient() { } NotificationPresenter* BrowserClient::GetNotificationPresenter() { - #if defined(OS_WIN) - // Bail out if on Windows 7 or even lower, no operating will follow - if (base::win::GetVersion() < base::win::VERSION_WIN8) - return nullptr; - #endif - if (!notification_presenter_) { // Create a new presenter if on OS X, Linux, or Windows 8+ notification_presenter_.reset(NotificationPresenter::Create()); diff --git a/brightray/browser/linux/libnotify_notification.cc b/brightray/browser/linux/libnotify_notification.cc index aea7b7cc42d..dad3acb4335 100644 --- a/brightray/browser/linux/libnotify_notification.cc +++ b/brightray/browser/linux/libnotify_notification.cc @@ -55,12 +55,6 @@ void log_and_clear_error(GError* error, const char* context) { } // namespace -// static -Notification* Notification::Create(NotificationDelegate* delegate, - NotificationPresenter* presenter) { - return new LibnotifyNotification(delegate, presenter); -} - // static bool LibnotifyNotification::Initialize() { if (!libnotify_loader_.Load("libnotify.so.4") && // most common one diff --git a/brightray/browser/linux/notification_presenter_linux.cc b/brightray/browser/linux/notification_presenter_linux.cc index c846fca6659..6df5ebaaec9 100644 --- a/brightray/browser/linux/notification_presenter_linux.cc +++ b/brightray/browser/linux/notification_presenter_linux.cc @@ -22,4 +22,9 @@ NotificationPresenterLinux::NotificationPresenterLinux() { NotificationPresenterLinux::~NotificationPresenterLinux() { } +Notification* NotificationPresenterLinux::CreateNotificationObject( + NotificationDelegate* delegate) { + return new LibnotifyNotification(delegate, this); +} + } // namespace brightray diff --git a/brightray/browser/linux/notification_presenter_linux.h b/brightray/browser/linux/notification_presenter_linux.h index ef436799484..a90f31ea60e 100644 --- a/brightray/browser/linux/notification_presenter_linux.h +++ b/brightray/browser/linux/notification_presenter_linux.h @@ -16,6 +16,9 @@ class NotificationPresenterLinux : public NotificationPresenter { ~NotificationPresenterLinux(); private: + Notification* CreateNotificationObject( + NotificationDelegate* delegate) override; + DISALLOW_COPY_AND_ASSIGN(NotificationPresenterLinux); }; diff --git a/brightray/browser/mac/cocoa_notification.mm b/brightray/browser/mac/cocoa_notification.mm index 4e9ee3238b8..f4599487c84 100644 --- a/brightray/browser/mac/cocoa_notification.mm +++ b/brightray/browser/mac/cocoa_notification.mm @@ -12,12 +12,6 @@ namespace brightray { -// static -Notification* Notification::Create(NotificationDelegate* delegate, - NotificationPresenter* presenter) { - return new CocoaNotification(delegate, presenter); -} - CocoaNotification::CocoaNotification(NotificationDelegate* delegate, NotificationPresenter* presenter) : Notification(delegate, presenter) { diff --git a/brightray/browser/mac/notification_presenter_mac.h b/brightray/browser/mac/notification_presenter_mac.h index 825a1dada23..514a27625ea 100644 --- a/brightray/browser/mac/notification_presenter_mac.h +++ b/brightray/browser/mac/notification_presenter_mac.h @@ -22,6 +22,9 @@ class NotificationPresenterMac : public NotificationPresenter { ~NotificationPresenterMac(); private: + Notification* CreateNotificationObject( + NotificationDelegate* delegate) override; + base::scoped_nsobject notification_center_delegate_; diff --git a/brightray/browser/mac/notification_presenter_mac.mm b/brightray/browser/mac/notification_presenter_mac.mm index a37e9182fc6..fec4c1a0539 100644 --- a/brightray/browser/mac/notification_presenter_mac.mm +++ b/brightray/browser/mac/notification_presenter_mac.mm @@ -35,4 +35,9 @@ NotificationPresenterMac::~NotificationPresenterMac() { NSUserNotificationCenter.defaultUserNotificationCenter.delegate = nil; } +Notification* NotificationPresenterMac::CreateNotificationObject( + NotificationDelegate* delegate) { + return new CocoaNotification(delegate, this); +} + } // namespace brightray diff --git a/brightray/browser/notification.h b/brightray/browser/notification.h index 87096bbe263..a330bc25d1b 100644 --- a/brightray/browser/notification.h +++ b/brightray/browser/notification.h @@ -47,16 +47,11 @@ class Notification { protected: Notification(NotificationDelegate* delegate, NotificationPresenter* presenter); + + public: virtual ~Notification(); private: - friend class NotificationPresenter; - - // Can only be called by NotificationPresenter, the caller is responsible of - // freeing the returned instance. - static Notification* Create(NotificationDelegate* delegate, - NotificationPresenter* presenter); - NotificationDelegate* delegate_; NotificationPresenter* presenter_; diff --git a/brightray/browser/notification_presenter.cc b/brightray/browser/notification_presenter.cc index ad46e292a27..30ef3b16b4b 100644 --- a/brightray/browser/notification_presenter.cc +++ b/brightray/browser/notification_presenter.cc @@ -18,7 +18,7 @@ NotificationPresenter::~NotificationPresenter() { base::WeakPtr NotificationPresenter::CreateNotification( NotificationDelegate* delegate) { - Notification* notification = Notification::Create(delegate, this); + Notification* notification = CreateNotificationObject(delegate); notifications_.insert(notification); return notification->GetWeakPtr(); } diff --git a/brightray/browser/notification_presenter.h b/brightray/browser/notification_presenter.h index b3dac3005dc..721ba92ace4 100644 --- a/brightray/browser/notification_presenter.h +++ b/brightray/browser/notification_presenter.h @@ -27,6 +27,8 @@ class NotificationPresenter { protected: NotificationPresenter(); + virtual Notification* CreateNotificationObject( + NotificationDelegate* delegate) = 0; private: friend class Notification; diff --git a/brightray/browser/win/notification_presenter_win.cc b/brightray/browser/win/notification_presenter_win.cc index c185246809e..930775c792b 100644 --- a/brightray/browser/win/notification_presenter_win.cc +++ b/brightray/browser/win/notification_presenter_win.cc @@ -10,6 +10,7 @@ #include "base/md5.h" #include "base/strings/utf_string_conversions.h" #include "base/win/windows_version.h" +#include "browser/win/notification_presenter_win7.h" #include "browser/win/windows_toast_notification.h" #include "content/public/browser/desktop_notification_delegate.h" #include "content/public/common/platform_notification_data.h" @@ -36,6 +37,9 @@ bool SaveIconToPath(const SkBitmap& bitmap, const base::FilePath& path) { // static NotificationPresenter* NotificationPresenter::Create() { + auto version = base::win::GetVersion(); + if (version < base::win::VERSION_WIN8) + return new NotificationPresenterWin7; if (!WindowsToastNotification::Initialize()) return nullptr; std::unique_ptr presenter( @@ -66,4 +70,9 @@ base::string16 NotificationPresenterWin::SaveIconToFilesystem( return base::UTF8ToUTF16(origin.spec()); } +Notification* NotificationPresenterWin::CreateNotificationObject( + NotificationDelegate* delegate) { + return new WindowsToastNotification(delegate, this); +} + } // namespace brightray diff --git a/brightray/browser/win/notification_presenter_win.h b/brightray/browser/win/notification_presenter_win.h index c3e6a9ad43f..679490c1852 100644 --- a/brightray/browser/win/notification_presenter_win.h +++ b/brightray/browser/win/notification_presenter_win.h @@ -42,6 +42,9 @@ class NotificationPresenterWin : public NotificationPresenter { base::string16 SaveIconToFilesystem(const SkBitmap& icon, const GURL& origin); private: + Notification* CreateNotificationObject( + NotificationDelegate* delegate) override; + base::ScopedTempDir temp_dir_; DISALLOW_COPY_AND_ASSIGN(NotificationPresenterWin); diff --git a/brightray/browser/win/notification_presenter_win7.cc b/brightray/browser/win/notification_presenter_win7.cc new file mode 100644 index 00000000000..f002c476d5a --- /dev/null +++ b/brightray/browser/win/notification_presenter_win7.cc @@ -0,0 +1,45 @@ +#include "browser/win/notification_presenter_win7.h" +#include "browser/win/win32_notification.h" + +namespace brightray { + +brightray::Notification* NotificationPresenterWin7::CreateNotificationObject( + NotificationDelegate* delegate) { + return new Win32Notification(delegate, this); +} + +Win32Notification* NotificationPresenterWin7::GetNotificationObjectByRef( + const DesktopNotificationController::Notification& ref) { + for (auto n : this->notifications()) { + auto w32n = static_cast(n); + if (w32n->GetRef() == ref) + return w32n; + } + + return nullptr; +} + +Win32Notification* NotificationPresenterWin7::GetNotificationObjectByTag( + const std::string& tag) { + for (auto n : this->notifications()) { + auto w32n = static_cast(n); + if (w32n->GetTag() == tag) + return w32n; + } + + return nullptr; +} + +void NotificationPresenterWin7::OnNotificationClicked( + Notification& notification) { + auto n = GetNotificationObjectByRef(notification); + if (n) n->NotificationClicked(); +} + +void NotificationPresenterWin7::OnNotificationDismissed( + Notification& notification) { + auto n = GetNotificationObjectByRef(notification); + if (n) n->NotificationDismissed(); +} + +} // namespace brightray diff --git a/brightray/browser/win/notification_presenter_win7.h b/brightray/browser/win/notification_presenter_win7.h new file mode 100644 index 00000000000..c191439befc --- /dev/null +++ b/brightray/browser/win/notification_presenter_win7.h @@ -0,0 +1,30 @@ +#pragma once +#include "browser/notification_presenter.h" +#include "browser/win/win32_desktop_notifications/desktop_notification_controller.h" + +namespace brightray { + +class Win32Notification; + +class NotificationPresenterWin7 : + public NotificationPresenter, + public DesktopNotificationController { + public: + NotificationPresenterWin7() = default; + + Win32Notification* GetNotificationObjectByRef( + const DesktopNotificationController::Notification& ref); + + Win32Notification* GetNotificationObjectByTag(const std::string& tag); + + private: + brightray::Notification* CreateNotificationObject( + NotificationDelegate* delegate) override; + + void OnNotificationClicked(Notification& notification) override; + void OnNotificationDismissed(Notification& notification) override; + + DISALLOW_COPY_AND_ASSIGN(NotificationPresenterWin7); +}; + +} // namespace brightray diff --git a/brightray/browser/win/win32_desktop_notifications/common.h b/brightray/browser/win/win32_desktop_notifications/common.h new file mode 100644 index 00000000000..ef364ceb4cc --- /dev/null +++ b/brightray/browser/win/win32_desktop_notifications/common.h @@ -0,0 +1,55 @@ +#pragma once +#include + +namespace brightray { + +struct NotificationData { + DesktopNotificationController* controller = nullptr; + + std::wstring caption; + std::wstring body_text; + HBITMAP image = NULL; + + + NotificationData() = default; + + ~NotificationData() { + if (image) DeleteObject(image); + } + + NotificationData(const NotificationData& other) = delete; + NotificationData& operator=(const NotificationData& other) = delete; +}; + +template +inline T ScaleForDpi(T value, unsigned dpi) { + return value * dpi / 96; +} + +struct ScreenMetrics { + UINT dpi_x, dpi_y; + + ScreenMetrics() { + typedef HRESULT WINAPI GetDpiForMonitor_t(HMONITOR, int, UINT*, UINT*); + + auto GetDpiForMonitor = reinterpret_cast( + GetProcAddress(GetModuleHandle(TEXT("shcore")), + "GetDpiForMonitor")); + + if (GetDpiForMonitor) { + auto monitor = MonitorFromPoint({}, MONITOR_DEFAULTTOPRIMARY); + if (GetDpiForMonitor(monitor, 0, &dpi_x, &dpi_y) == S_OK) + return; + } + + HDC hdc = GetDC(NULL); + dpi_x = GetDeviceCaps(hdc, LOGPIXELSX); + dpi_y = GetDeviceCaps(hdc, LOGPIXELSY); + ReleaseDC(NULL, hdc); + } + + template T X(T value) const { return ScaleForDpi(value, dpi_x); } + template T Y(T value) const { return ScaleForDpi(value, dpi_y); } +}; + +} // namespace brightray diff --git a/brightray/browser/win/win32_desktop_notifications/desktop_notification_controller.cc b/brightray/browser/win/win32_desktop_notifications/desktop_notification_controller.cc new file mode 100644 index 00000000000..68068a6de26 --- /dev/null +++ b/brightray/browser/win/win32_desktop_notifications/desktop_notification_controller.cc @@ -0,0 +1,404 @@ +#define NOMINMAX +#define WIN32_LEAN_AND_MEAN +#include "browser/win/win32_desktop_notifications/desktop_notification_controller.h" +#include +#include +#include +#include "browser/win/win32_desktop_notifications/common.h" +#include "browser/win/win32_desktop_notifications/toast.h" + +using std::make_shared; +using std::shared_ptr; + +namespace brightray { + +HBITMAP CopyBitmap(HBITMAP bitmap) { + HBITMAP ret = NULL; + + BITMAP bm; + if (bitmap && GetObject(bitmap, sizeof(bm), &bm)) { + HDC hdc_screen = GetDC(NULL); + ret = CreateCompatibleBitmap(hdc_screen, bm.bmWidth, bm.bmHeight); + ReleaseDC(NULL, hdc_screen); + + if (ret) { + HDC hdc_src = CreateCompatibleDC(NULL); + HDC hdc_dst = CreateCompatibleDC(NULL); + SelectBitmap(hdc_src, bitmap); + SelectBitmap(hdc_dst, ret); + BitBlt(hdc_dst, 0, 0, bm.bmWidth, bm.bmHeight, + hdc_src, 0, 0, SRCCOPY); + DeleteDC(hdc_dst); + DeleteDC(hdc_src); + } + } + + return ret; +} + +HINSTANCE DesktopNotificationController::RegisterWndClasses() { + // We keep a static `module` variable which serves a dual purpose: + // 1. Stores the HINSTANCE where the window classes are registered, + // which can be passed to `CreateWindow` + // 2. Indicates whether we already attempted the registration so that + // we don't do it twice (we don't retry even if registration fails, + // as there is no point). + static HMODULE module = NULL; + + if (!module) { + if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&RegisterWndClasses), + &module)) { + Toast::Register(module); + + WNDCLASSEX wc = { sizeof(wc) }; + wc.lpfnWndProc = &WndProc; + wc.lpszClassName = class_name_; + wc.cbWndExtra = sizeof(DesktopNotificationController*); + wc.hInstance = module; + + RegisterClassEx(&wc); + } + } + + return module; +} + +DesktopNotificationController::DesktopNotificationController( + unsigned maximum_toasts) { + instances_.reserve(maximum_toasts); +} + +DesktopNotificationController::~DesktopNotificationController() { + for (auto&& inst : instances_) DestroyToast(inst); + if (hwnd_controller_) DestroyWindow(hwnd_controller_); + ClearAssets(); +} + +LRESULT CALLBACK DesktopNotificationController::WndProc( + HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + switch (message) { + case WM_CREATE: + { + auto& cs = reinterpret_cast(lparam); + SetWindowLongPtr(hwnd, 0, (LONG_PTR)cs->lpCreateParams); + } + break; + + case WM_TIMER: + if (wparam == TimerID_Animate) { + Get(hwnd)->AnimateAll(); + } + return 0; + + case WM_DISPLAYCHANGE: + { + auto inst = Get(hwnd); + inst->ClearAssets(); + inst->AnimateAll(); + } + break; + + case WM_SETTINGCHANGE: + if (wparam == SPI_SETWORKAREA) { + Get(hwnd)->AnimateAll(); + } + break; + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +void DesktopNotificationController::StartAnimation() { + _ASSERT(hwnd_controller_); + + if (!is_animating_ && hwnd_controller_) { + // NOTE: 15ms is shorter than what we'd need for 60 fps, but since + // the timer is not accurate we must request a higher frame rate + // to get at least 60 + + SetTimer(hwnd_controller_, TimerID_Animate, 15, nullptr); + is_animating_ = true; + } +} + +HFONT DesktopNotificationController::GetCaptionFont() { + InitializeFonts(); + return caption_font_; +} + +HFONT DesktopNotificationController::GetBodyFont() { + InitializeFonts(); + return body_font_; +} + +void DesktopNotificationController::InitializeFonts() { + if (!body_font_) { + NONCLIENTMETRICS metrics = { sizeof(metrics) }; + if (SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &metrics, 0)) { + auto baseHeight = metrics.lfMessageFont.lfHeight; + + HDC hdc = GetDC(NULL); + auto dpi_y = GetDeviceCaps(hdc, LOGPIXELSY); + ReleaseDC(NULL, hdc); + + metrics.lfMessageFont.lfHeight = + (LONG)ScaleForDpi(baseHeight * 1.1f, dpi_y); + body_font_ = CreateFontIndirect(&metrics.lfMessageFont); + + if (caption_font_) DeleteFont(caption_font_); + metrics.lfMessageFont.lfHeight = + (LONG)ScaleForDpi(baseHeight * 1.4f, dpi_y); + caption_font_ = CreateFontIndirect(&metrics.lfMessageFont); + } + } +} + +void DesktopNotificationController::ClearAssets() { + if (caption_font_) { DeleteFont(caption_font_); caption_font_ = NULL; } + if (body_font_) { DeleteFont(body_font_); body_font_ = NULL; } +} + +void DesktopNotificationController::AnimateAll() { + // NOTE: This function refreshes position and size of all toasts according + // to all current conditions. Animation time is only one of the variables + // influencing them. Screen resolution is another. + + bool keep_animating = false; + + if (!instances_.empty()) { + RECT work_area; + if (SystemParametersInfo(SPI_GETWORKAREA, 0, &work_area, 0)) { + ScreenMetrics metrics; + POINT origin = { work_area.right, + work_area.bottom - metrics.Y(toast_margin_) }; + + auto hdwp = + BeginDeferWindowPos(static_cast(instances_.size())); + + for (auto&& inst : instances_) { + if (!inst.hwnd) continue; + + auto notification = Toast::Get(inst.hwnd); + hdwp = notification->Animate(hdwp, origin); + if (!hdwp) break; + keep_animating |= notification->IsAnimationActive(); + } + + if (hdwp) EndDeferWindowPos(hdwp); + } + } + + if (!keep_animating) { + _ASSERT(hwnd_controller_); + if (hwnd_controller_) KillTimer(hwnd_controller_, TimerID_Animate); + is_animating_ = false; + } + + // Purge dismissed notifications and collapse the stack between + // items which are highlighted + if (!instances_.empty()) { + auto is_alive = [](ToastInstance& inst) { + return inst.hwnd && IsWindowVisible(inst.hwnd); + }; + + auto is_highlighted = [](ToastInstance& inst) { + return inst.hwnd && Toast::Get(inst.hwnd)->IsHighlighted(); + }; + + for (auto it = instances_.begin();; ++it) { + // find next highlighted item + auto it2 = find_if(it, instances_.end(), is_highlighted); + + // collapse the stack in front of the highlighted item + it = stable_partition(it, it2, is_alive); + + // purge the dead items + for_each(it, it2, [this](auto&& inst) { DestroyToast(inst); }); + + if (it2 == instances_.end()) { + instances_.erase(it, it2); + break; + } + + it = move(it2); + } + } + + // Set new toast positions + if (!instances_.empty()) { + ScreenMetrics metrics; + auto margin = metrics.Y(toast_margin_); + + int target_pos = 0; + for (auto&& inst : instances_) { + if (inst.hwnd) { + auto toast = Toast::Get(inst.hwnd); + + if (toast->IsHighlighted()) + target_pos = toast->GetVerticalPosition(); + else + toast->SetVerticalPosition(target_pos); + + target_pos += toast->GetHeight() + margin; + } + } + } + + // Create new toasts from the queue + CheckQueue(); +} + +DesktopNotificationController::Notification + DesktopNotificationController::AddNotification( + std::wstring caption, std::wstring body_text, HBITMAP image) { + NotificationLink data(this); + + data->caption = move(caption); + data->body_text = move(body_text); + data->image = CopyBitmap(image); + + // Enqueue new notification + Notification ret { *queue_.insert(queue_.end(), move(data)) }; + CheckQueue(); + return ret; +} + +void DesktopNotificationController::CloseNotification( + Notification& notification) { + // Remove it from the queue + auto it = find(queue_.begin(), queue_.end(), notification.data_); + if (it != queue_.end()) { + queue_.erase(it); + this->OnNotificationClosed(notification); + return; + } + + // Dismiss active toast + auto hwnd = GetToast(notification.data_.get()); + if (hwnd) { + auto toast = Toast::Get(hwnd); + toast->Dismiss(); + } +} + +void DesktopNotificationController::CheckQueue() { + while (instances_.size() < instances_.capacity() && !queue_.empty()) { + CreateToast(move(queue_.front())); + queue_.pop_front(); + } +} + +void DesktopNotificationController::CreateToast(NotificationLink&& data) { + auto hinstance = RegisterWndClasses(); + auto hwnd = Toast::Create(hinstance, data); + if (hwnd) { + int toast_pos = 0; + if (!instances_.empty()) { + auto& item = instances_.back(); + _ASSERT(item.hwnd); + + ScreenMetrics scr; + auto toast = Toast::Get(item.hwnd); + toast_pos = toast->GetVerticalPosition() + + toast->GetHeight() + + scr.Y(toast_margin_); + } + + instances_.push_back({ hwnd, move(data) }); + + if (!hwnd_controller_) { + // NOTE: We cannot use a message-only window because we need to + // receive system notifications + hwnd_controller_ = CreateWindow(class_name_, nullptr, 0, + 0, 0, 0, 0, + NULL, NULL, hinstance, this); + } + + auto toast = Toast::Get(hwnd); + toast->PopUp(toast_pos); + } +} + +HWND DesktopNotificationController::GetToast( + const NotificationData* data) const { + auto it = find_if(instances_.cbegin(), instances_.cend(), + [data](auto&& inst) { + auto toast = Toast::Get(inst.hwnd); + return data == toast->GetNotification().get(); + }); + + return (it != instances_.cend()) ? it->hwnd : NULL; +} + +void DesktopNotificationController::DestroyToast(ToastInstance& inst) { + if (inst.hwnd) { + auto data = Toast::Get(inst.hwnd)->GetNotification(); + + DestroyWindow(inst.hwnd); + inst.hwnd = NULL; + + Notification notification(data); + OnNotificationClosed(notification); + } +} + + +DesktopNotificationController::Notification::Notification( + const shared_ptr& data) : + data_(data) { + _ASSERT(data != nullptr); +} + +bool DesktopNotificationController::Notification::operator==( + const Notification& other) const { + return data_ == other.data_; +} + +void DesktopNotificationController::Notification::Close() { + // No business calling this when not pointing to a valid instance + _ASSERT(data_); + + if (data_->controller) + data_->controller->CloseNotification(*this); +} + +void DesktopNotificationController::Notification::Set( + std::wstring caption, std::wstring body_text, HBITMAP image) { + // No business calling this when not pointing to a valid instance + _ASSERT(data_); + + // Do nothing when the notification has been closed + if (!data_->controller) + return; + + if (data_->image) DeleteBitmap(data_->image); + + data_->caption = move(caption); + data_->body_text = move(body_text); + data_->image = CopyBitmap(image); + + auto hwnd = data_->controller->GetToast(data_.get()); + if (hwnd) { + auto toast = Toast::Get(hwnd); + toast->ResetContents(); + } + + // Change of contents can affect size and position of all toasts + data_->controller->StartAnimation(); +} + + +DesktopNotificationController::NotificationLink::NotificationLink( + DesktopNotificationController* controller) : + shared_ptr(make_shared()) { + get()->controller = controller; +} + +DesktopNotificationController::NotificationLink::~NotificationLink() { + auto p = get(); + if (p) p->controller = nullptr; +} + +} // namespace brightray diff --git a/brightray/browser/win/win32_desktop_notifications/desktop_notification_controller.h b/brightray/browser/win/win32_desktop_notifications/desktop_notification_controller.h new file mode 100644 index 00000000000..643a61f5331 --- /dev/null +++ b/brightray/browser/win/win32_desktop_notifications/desktop_notification_controller.h @@ -0,0 +1,106 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace brightray { + +struct NotificationData; + +class DesktopNotificationController { + public: + explicit DesktopNotificationController(unsigned maximum_toasts = 3); + ~DesktopNotificationController(); + + class Notification; + Notification AddNotification(std::wstring caption, std::wstring body_text, + HBITMAP image); + void CloseNotification(Notification& notification); + + // Event handlers -- override to receive the events + private: + virtual void OnNotificationClosed(Notification& notification) {} + virtual void OnNotificationClicked(Notification& notification) {} + virtual void OnNotificationDismissed(Notification& notification) {} + + private: + static HINSTANCE RegisterWndClasses(); + void StartAnimation(); + HFONT GetCaptionFont(); + HFONT GetBodyFont(); + + private: + enum TimerID { + TimerID_Animate = 1 + }; + + template + static constexpr T toast_margin_ = 20; + + // Wrapper around `NotificationData` which makes sure that + // the `controller` member is cleared when the controller object + // stops tracking the notification + struct NotificationLink : std::shared_ptr { + explicit NotificationLink(DesktopNotificationController* controller); + ~NotificationLink(); + + NotificationLink(NotificationLink&&) = default; + NotificationLink(const NotificationLink&) = delete; + NotificationLink& operator=(NotificationLink&&) = default; + }; + + struct ToastInstance { + HWND hwnd; + NotificationLink data; + }; + + class Toast; + + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam); + static DesktopNotificationController* Get(HWND hwnd) { + return reinterpret_cast( + GetWindowLongPtr(hwnd, 0)); + } + + DesktopNotificationController( + const DesktopNotificationController&) = delete; + + void InitializeFonts(); + void ClearAssets(); + void AnimateAll(); + void CheckQueue(); + void CreateToast(NotificationLink&& data); + HWND GetToast(const NotificationData* data) const; + void DestroyToast(ToastInstance& inst); + + private: + static constexpr const TCHAR class_name_[] = + TEXT("DesktopNotificationController"); + + HWND hwnd_controller_ = NULL; + HFONT caption_font_ = NULL, body_font_ = NULL; + std::vector instances_; + std::deque queue_; + bool is_animating_ = false; +}; + +class DesktopNotificationController::Notification { + public: + Notification() = default; + explicit Notification(const std::shared_ptr& data); + + bool operator==(const Notification& other) const; + + void Close(); + void Set(std::wstring caption, std::wstring body_text, HBITMAP image); + + private: + std::shared_ptr data_; + + friend class DesktopNotificationController; +}; + +} // namespace brightray diff --git a/brightray/browser/win/win32_desktop_notifications/toast.cc b/brightray/browser/win/win32_desktop_notifications/toast.cc new file mode 100644 index 00000000000..1ab968c3ef0 --- /dev/null +++ b/brightray/browser/win/win32_desktop_notifications/toast.cc @@ -0,0 +1,822 @@ +#define NOMINMAX +#include "browser/win/win32_desktop_notifications/toast.h" +#include +#include +#include +#include "browser/win/win32_desktop_notifications/common.h" + +#pragma comment(lib, "msimg32.lib") +#pragma comment(lib, "uxtheme.lib") + +using std::min; +using std::shared_ptr; + +namespace brightray { + +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(&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(&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(&bmi), + DIB_RGB_COLORS, &alpha_src_bits, NULL, 0); + + if (alpha_src_bitmap) { + if (GetDIBits(hdc_screen, bitmap, 0, 0, 0, + reinterpret_cast(&bmi), + DIB_RGB_COLORS) && + bmi.biSizeImage > 0 && + (bmi.biSizeImage % 4) == 0) { + auto buf = reinterpret_cast( + _aligned_malloc(bmi.biSizeImage, sizeof(DWORD))); + + if (buf) { + GetDIBits(hdc_screen, bitmap, 0, bm.bmHeight, buf, + reinterpret_cast(&bmi), + DIB_RGB_COLORS); + + const DWORD *src = reinterpret_cast(buf); + const DWORD *end = + reinterpret_cast(buf + bmi.biSizeImage); + + BYTE* dest = reinterpret_cast(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(&bmi), + DIB_RGB_COLORS, &color_bits, NULL, 0); + + void* alpha_bits; + auto alpha_bitmap = + CreateDIBSection(NULL, reinterpret_cast(&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(color_bits); + auto src = reinterpret_cast(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(&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; +} + +DesktopNotificationController::Toast::Toast( + HWND hwnd, shared_ptr* data) : + hwnd_(hwnd), data_(*data) { + HDC hdc_screen = GetDC(NULL); + hdc_ = CreateCompatibleDC(hdc_screen); + ReleaseDC(NULL, hdc_screen); +} + +DesktopNotificationController::Toast::~Toast() { + 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(lparam); + auto data = + static_cast*>(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_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(lparam); + if (wp->flags & SWP_HIDEWINDOW) { + if (!IsWindowVisible(hwnd)) + Get(hwnd)->is_highlighted_ = false; + } + } + break; + } + + return DefWindowProc(hwnd, message, wparam, lparam); +} + +HWND DesktopNotificationController::Toast::Create( + HINSTANCE hinstance, shared_ptr& 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); + _ASSERT(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() { + SetTimer(hwnd_, TimerID_AutoDismiss, 4000, 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( + (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(toast_size_.cx * ease_in_pos); + size.cy = toast_size_.cy; + + pt.x = origin.x - size.cx; + pt.y = static_cast(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). + + auto ulw_result = UpdateLayeredWindowIndirect(hwnd_, &ulw); + hdwp = DeferWindowPos(hdwp, hwnd_, HWND_TOPMOST, + pt.x, pt.y, size.cx, size.cy, dwpFlags); + return hdwp; +} + +void DesktopNotificationController::Toast::StartEaseIn() { + _ASSERT(!ease_in_active_); + ease_in_start_ = GetTickCount(); + ease_in_active_ = true; + data_->controller->StartAnimation(); +} + +void DesktopNotificationController::Toast::StartEaseOut() { + _ASSERT(!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 float duration = 500.0f; + float elapsed = GetTickCount() - ease_in_start_; + float time = std::min(duration, elapsed) / 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 float duration = 120.0f; + float elapsed = GetTickCount() - ease_out_start_; + float time = std::min(duration, elapsed) / 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 float duration = 500.0f; + float elapsed = GetTickCount() - stack_collapse_start_; + float time = std::min(duration, elapsed) / 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 brightray diff --git a/brightray/browser/win/win32_desktop_notifications/toast.h b/brightray/browser/win/win32_desktop_notifications/toast.h new file mode 100644 index 00000000000..bb9bd9fa4ae --- /dev/null +++ b/brightray/browser/win/win32_desktop_notifications/toast.h @@ -0,0 +1,97 @@ +#pragma once +#include "browser/win/win32_desktop_notifications/desktop_notification_controller.h" + +namespace brightray { + +class DesktopNotificationController::Toast { + public: + static void Register(HINSTANCE hinstance); + static HWND Create(HINSTANCE hinstance, + std::shared_ptr& data); + static Toast* Get(HWND hwnd) { + return reinterpret_cast(GetWindowLongPtr(hwnd, 0)); + } + + static LRESULT CALLBACK WndProc(HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam); + + const std::shared_ptr& GetNotification() const { + return data_; + } + + void ResetContents(); + + void Dismiss(); + + void PopUp(int y); + void SetVerticalPosition(int y); + int GetVerticalPosition() const { + return vertical_pos_target_; + } + int GetHeight() const { + return toast_size_.cy; + } + HDWP Animate(HDWP hdwp, const POINT& origin); + bool IsAnimationActive() const { + return ease_in_active_ || ease_out_active_ || IsStackCollapseActive(); + } + bool IsHighlighted() const { + _ASSERT(!(is_highlighted_ && !IsWindowVisible(hwnd_))); + return is_highlighted_; + } + + private: + enum TimerID { + TimerID_AutoDismiss = 1 + }; + + Toast(HWND hwnd, std::shared_ptr* data); + ~Toast(); + + void UpdateBufferSize(); + void UpdateScaledImage(const SIZE& size); + void Draw(); + void Invalidate(); + bool IsRedrawNeeded() const; + void UpdateContents(); + + void AutoDismiss(); + void CancelDismiss(); + void ScheduleDismissal(); + + void StartEaseIn(); + void StartEaseOut(); + bool IsStackCollapseActive() const; + + float AnimateEaseIn(); + float AnimateEaseOut(); + float AnimateStackCollapse(); + + private: + static constexpr const TCHAR class_name_[] = + TEXT("DesktopNotificationToast"); + + const HWND hwnd_; + HDC hdc_; + HBITMAP bitmap_ = NULL; + + const std::shared_ptr data_; // never null + + SIZE toast_size_ = {}; + SIZE margin_ = {}; + RECT close_button_rect_ = {}; + HBITMAP scaled_image_ = NULL; + + int vertical_pos_ = 0; + int vertical_pos_target_ = 0; + bool is_non_interactive_ = false; + bool ease_in_active_ = false; + bool ease_out_active_ = false; + bool is_content_updated_ = false; + bool is_highlighted_ = false; + bool is_close_hot_ = false; + DWORD ease_in_start_, ease_out_start_, stack_collapse_start_; + float ease_in_pos_ = 0, ease_out_pos_ = 0, stack_collapse_pos_ = 0; +}; + +} // namespace brightray diff --git a/brightray/browser/win/win32_notification.cc b/brightray/browser/win/win32_notification.cc new file mode 100644 index 00000000000..5b6093658aa --- /dev/null +++ b/brightray/browser/win/win32_notification.cc @@ -0,0 +1,58 @@ +#define WIN32_LEAN_AND_MEAN +#include "browser/win/win32_notification.h" +#include +#include "third_party/skia/include/core/SkBitmap.h" + +namespace brightray { + +void Win32Notification::Show( + const base::string16& title, const base::string16& msg, + const std::string& tag, const GURL& icon_url, + const SkBitmap& icon, const bool silent) { + auto presenter = static_cast(this->presenter()); + if (!presenter) return; + + HBITMAP image = NULL; + + if (!icon.drawsNothing()) { + if (icon.colorType() == kBGRA_8888_SkColorType) { + icon.lockPixels(); + + BITMAPINFOHEADER bmi = { sizeof(BITMAPINFOHEADER) }; + bmi.biWidth = icon.width(); + bmi.biHeight = -icon.height(); + bmi.biPlanes = 1; + bmi.biBitCount = 32; + bmi.biCompression = BI_RGB; + + HDC hdcScreen = GetDC(NULL); + image = CreateDIBitmap(hdcScreen, &bmi, CBM_INIT, icon.getPixels(), + reinterpret_cast(&bmi), + DIB_RGB_COLORS); + ReleaseDC(NULL, hdcScreen); + + icon.unlockPixels(); + } + } + + Win32Notification* existing = nullptr; + if (!tag.empty()) existing = presenter->GetNotificationObjectByTag(tag); + + if (existing) { + existing->tag_.clear(); + this->notification_ref_ = std::move(existing->notification_ref_); + this->notification_ref_.Set(title, msg, image); + } else { + this->notification_ref_ = presenter->AddNotification(title, msg, image); + } + + this->tag_ = tag; + + if (image) DeleteObject(image); +} + +void Win32Notification::Dismiss() { + notification_ref_.Close(); +} + +} // namespace brightray diff --git a/brightray/browser/win/win32_notification.h b/brightray/browser/win/win32_notification.h new file mode 100644 index 00000000000..9a418cd5037 --- /dev/null +++ b/brightray/browser/win/win32_notification.h @@ -0,0 +1,33 @@ +#pragma once +#include "browser/notification.h" +#include "browser/win/notification_presenter_win7.h" + +namespace brightray { + +class Win32Notification : public brightray::Notification { + public: + Win32Notification(NotificationDelegate* delegate, + NotificationPresenterWin7* presenter) : + Notification(delegate, presenter) { + } + void Show(const base::string16& title, const base::string16& msg, + const std::string& tag, const GURL& icon_url, + const SkBitmap& icon, const bool silent) override; + void Dismiss() override; + + const DesktopNotificationController::Notification& GetRef() const { + return notification_ref_; + } + + const std::string& GetTag() const { + return tag_; + } + + private: + DesktopNotificationController::Notification notification_ref_; + std::string tag_; + + DISALLOW_COPY_AND_ASSIGN(Win32Notification); +}; + +} // namespace brightray diff --git a/brightray/browser/win/windows_toast_notification.cc b/brightray/browser/win/windows_toast_notification.cc index 4a3a5a6f678..0e0c9249c11 100644 --- a/brightray/browser/win/windows_toast_notification.cc +++ b/brightray/browser/win/windows_toast_notification.cc @@ -42,12 +42,6 @@ bool GetAppUserModelId(ScopedHString* app_id) { } // namespace -// static -Notification* Notification::Create(NotificationDelegate* delegate, - NotificationPresenter* presenter) { - return new WindowsToastNotification(delegate, presenter); -} - // static ComPtr WindowsToastNotification::toast_manager_; diff --git a/brightray/filenames.gypi b/brightray/filenames.gypi index a0858ea926c..b7d44013b84 100644 --- a/brightray/filenames.gypi +++ b/brightray/filenames.gypi @@ -84,12 +84,21 @@ 'browser/linux/libnotify_notification.cc', 'browser/linux/notification_presenter_linux.h', 'browser/linux/notification_presenter_linux.cc', - 'browser/win/notification_presenter_win.h', 'browser/win/notification_presenter_win.cc', - 'browser/win/windows_toast_notification.h', - 'browser/win/windows_toast_notification.cc', - 'browser/win/scoped_hstring.h', + 'browser/win/notification_presenter_win.h', + 'browser/win/notification_presenter_win7.cc', + 'browser/win/notification_presenter_win7.h', 'browser/win/scoped_hstring.cc', + 'browser/win/scoped_hstring.h', + 'browser/win/win32_desktop_notifications/common.h', + 'browser/win/win32_desktop_notifications/desktop_notification_controller.cc', + 'browser/win/win32_desktop_notifications/desktop_notification_controller.h', + 'browser/win/win32_desktop_notifications/toast.cc', + 'browser/win/win32_desktop_notifications/toast.h', + 'browser/win/win32_notification.cc', + 'browser/win/win32_notification.h', + 'browser/win/windows_toast_notification.cc', + 'browser/win/windows_toast_notification.h', 'browser/special_storage_policy.cc', 'browser/special_storage_policy.h', 'browser/url_request_context_getter.cc',