diff options
Diffstat (limited to 'widget/windows/WinWindowOcclusionTracker.cpp')
-rw-r--r-- | widget/windows/WinWindowOcclusionTracker.cpp | 1481 |
1 files changed, 1481 insertions, 0 deletions
diff --git a/widget/windows/WinWindowOcclusionTracker.cpp b/widget/windows/WinWindowOcclusionTracker.cpp new file mode 100644 index 0000000000..4e382e70d5 --- /dev/null +++ b/widget/windows/WinWindowOcclusionTracker.cpp @@ -0,0 +1,1481 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <queue> +#include <windows.h> +#include <winuser.h> +#include <wtsapi32.h> + +#include "WinWindowOcclusionTracker.h" + +#include "base/thread.h" +#include "base/message_loop.h" +#include "base/platform_thread.h" +#include "gfxConfig.h" +#include "nsThreadUtils.h" +#include "mozilla/DataMutex.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Logging.h" +#include "mozilla/StaticPrefs_widget.h" +#include "mozilla/StaticPtr.h" +#include "nsBaseWidget.h" +#include "nsWindow.h" +#include "transport/runnable_utils.h" +#include "WinUtils.h" + +// Win 8+ (_WIN32_WINNT_WIN8) +#ifndef EVENT_OBJECT_CLOAKED +# define EVENT_OBJECT_CLOAKED 0x8017 +# define EVENT_OBJECT_UNCLOAKED 0x8018 +#endif + +namespace mozilla::widget { + +// Can be called on Main thread +LazyLogModule gWinOcclusionTrackerLog("WinOcclusionTracker"); +#define LOG(type, ...) MOZ_LOG(gWinOcclusionTrackerLog, type, (__VA_ARGS__)) + +// Can be called on OcclusionCalculator thread +LazyLogModule gWinOcclusionCalculatorLog("WinOcclusionCalculator"); +#define CALC_LOG(type, ...) \ + MOZ_LOG(gWinOcclusionCalculatorLog, type, (__VA_ARGS__)) + +// ~16 ms = time between frames when frame rate is 60 FPS. +const int kOcclusionUpdateRunnableDelayMs = 16; + +class OcclusionUpdateRunnable : public CancelableRunnable { + public: + explicit OcclusionUpdateRunnable( + WinWindowOcclusionTracker::WindowOcclusionCalculator* + aOcclusionCalculator) + : CancelableRunnable("OcclusionUpdateRunnable"), + mOcclusionCalculator(aOcclusionCalculator) { + mTimeStamp = TimeStamp::Now(); + } + + NS_IMETHOD Run() override { + if (mIsCanceled) { + return NS_OK; + } + MOZ_ASSERT(WinWindowOcclusionTracker::IsInWinWindowOcclusionThread()); + + uint32_t latencyMs = + round((TimeStamp::Now() - mTimeStamp).ToMilliseconds()); + CALC_LOG(LogLevel::Debug, + "ComputeNativeWindowOcclusionStatus() latencyMs %u", latencyMs); + + mOcclusionCalculator->ComputeNativeWindowOcclusionStatus(); + return NS_OK; + } + + nsresult Cancel() override { + mIsCanceled = true; + mOcclusionCalculator = nullptr; + return NS_OK; + } + + private: + bool mIsCanceled = false; + RefPtr<WinWindowOcclusionTracker::WindowOcclusionCalculator> + mOcclusionCalculator; + TimeStamp mTimeStamp; +}; + +// Used to serialize tasks related to mRootWindowHwndsOcclusionState. +class SerializedTaskDispatcher { + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SerializedTaskDispatcher) + + public: + SerializedTaskDispatcher(); + + void Destroy(); + void PostTaskToMain(already_AddRefed<nsIRunnable> aTask); + void PostTaskToCalculator(already_AddRefed<nsIRunnable> aTask); + void PostDelayedTaskToCalculator(already_AddRefed<Runnable> aTask, + int aDelayMs); + bool IsOnCurrentThread(); + + private: + friend class DelayedTaskRunnable; + + ~SerializedTaskDispatcher(); + + struct Data { + std::queue<std::pair<RefPtr<nsIRunnable>, RefPtr<nsISerialEventTarget>>> + mTasks; + bool mDestroyed = false; + RefPtr<Runnable> mCurrentRunnable; + }; + + void PostTasksIfNecessary(nsISerialEventTarget* aEventTarget, + const DataMutex<Data>::AutoLock& aProofOfLock); + void HandleDelayedTask(already_AddRefed<nsIRunnable> aTask); + void HandleTasks(); + + // Hold current EventTarget during calling nsIRunnable::Run(). + RefPtr<nsISerialEventTarget> mCurrentEventTarget = nullptr; + + DataMutex<Data> mData; +}; + +class DelayedTaskRunnable : public Runnable { + public: + DelayedTaskRunnable(SerializedTaskDispatcher* aSerializedTaskDispatcher, + already_AddRefed<Runnable> aTask) + : Runnable("DelayedTaskRunnable"), + mSerializedTaskDispatcher(aSerializedTaskDispatcher), + mTask(aTask) {} + + NS_IMETHOD Run() override { + mSerializedTaskDispatcher->HandleDelayedTask(mTask.forget()); + return NS_OK; + } + + private: + RefPtr<SerializedTaskDispatcher> mSerializedTaskDispatcher; + RefPtr<Runnable> mTask; +}; + +SerializedTaskDispatcher::SerializedTaskDispatcher() + : mData("SerializedTaskDispatcher::mData") { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "SerializedTaskDispatcher::SerializedTaskDispatcher() this %p", this); +} + +SerializedTaskDispatcher::~SerializedTaskDispatcher() { +#ifdef DEBUG + auto data = mData.Lock(); + MOZ_ASSERT(data->mDestroyed); + MOZ_ASSERT(data->mTasks.empty()); +#endif +} + +void SerializedTaskDispatcher::Destroy() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "SerializedTaskDispatcher::Destroy() this %p", this); + + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + data->mDestroyed = true; + std::queue<std::pair<RefPtr<nsIRunnable>, RefPtr<nsISerialEventTarget>>> + empty; + std::swap(data->mTasks, empty); +} + +void SerializedTaskDispatcher::PostTaskToMain( + already_AddRefed<nsIRunnable> aTask) { + RefPtr<nsIRunnable> task = aTask; + + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + nsISerialEventTarget* eventTarget = GetMainThreadSerialEventTarget(); + data->mTasks.push({std::move(task), eventTarget}); + + MOZ_ASSERT_IF(!data->mCurrentRunnable, data->mTasks.size() == 1); + PostTasksIfNecessary(eventTarget, data); +} + +void SerializedTaskDispatcher::PostTaskToCalculator( + already_AddRefed<nsIRunnable> aTask) { + RefPtr<nsIRunnable> task = aTask; + + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + nsISerialEventTarget* eventTarget = + WinWindowOcclusionTracker::OcclusionCalculatorLoop()->SerialEventTarget(); + data->mTasks.push({std::move(task), eventTarget}); + + MOZ_ASSERT_IF(!data->mCurrentRunnable, data->mTasks.size() == 1); + PostTasksIfNecessary(eventTarget, data); +} + +void SerializedTaskDispatcher::PostDelayedTaskToCalculator( + already_AddRefed<Runnable> aTask, int aDelayMs) { + CALC_LOG(LogLevel::Debug, + "SerializedTaskDispatcher::PostDelayedTaskToCalculator()"); + + RefPtr<DelayedTaskRunnable> runnable = + new DelayedTaskRunnable(this, std::move(aTask)); + MessageLoop* targetLoop = + WinWindowOcclusionTracker::OcclusionCalculatorLoop(); + targetLoop->PostDelayedTask(runnable.forget(), aDelayMs); +} + +bool SerializedTaskDispatcher::IsOnCurrentThread() { + return !!mCurrentEventTarget; +} + +void SerializedTaskDispatcher::PostTasksIfNecessary( + nsISerialEventTarget* aEventTarget, + const DataMutex<Data>::AutoLock& aProofOfLock) { + MOZ_ASSERT(!aProofOfLock->mTasks.empty()); + + if (aProofOfLock->mCurrentRunnable) { + return; + } + + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<SerializedTaskDispatcher>(this), + &SerializedTaskDispatcher::HandleTasks); + aProofOfLock->mCurrentRunnable = runnable; + aEventTarget->Dispatch(runnable.forget()); +} + +void SerializedTaskDispatcher::HandleDelayedTask( + already_AddRefed<nsIRunnable> aTask) { + MOZ_ASSERT(WinWindowOcclusionTracker::IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Debug, "SerializedTaskDispatcher::HandleDelayedTask()"); + + RefPtr<nsIRunnable> task = aTask; + + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + nsISerialEventTarget* eventTarget = + WinWindowOcclusionTracker::OcclusionCalculatorLoop()->SerialEventTarget(); + data->mTasks.push({std::move(task), eventTarget}); + + MOZ_ASSERT_IF(!data->mCurrentRunnable, data->mTasks.size() == 1); + PostTasksIfNecessary(eventTarget, data); +} + +void SerializedTaskDispatcher::HandleTasks() { + RefPtr<nsIRunnable> frontTask; + + // Get front task + { + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + MOZ_RELEASE_ASSERT(data->mCurrentRunnable); + MOZ_RELEASE_ASSERT(!data->mTasks.empty()); + + frontTask = data->mTasks.front().first; + + MOZ_RELEASE_ASSERT(!mCurrentEventTarget); + mCurrentEventTarget = data->mTasks.front().second; + } + + while (frontTask) { + if (NS_IsMainThread()) { + LOG(LogLevel::Debug, "SerializedTaskDispatcher::HandleTasks()"); + } else { + CALC_LOG(LogLevel::Debug, "SerializedTaskDispatcher::HandleTasks()"); + } + + MOZ_ASSERT_IF(NS_IsMainThread(), + mCurrentEventTarget == GetMainThreadSerialEventTarget()); + MOZ_ASSERT_IF( + !NS_IsMainThread(), + mCurrentEventTarget == MessageLoop::current()->SerialEventTarget()); + + frontTask->Run(); + + // Get next task + { + auto data = mData.Lock(); + if (data->mDestroyed) { + return; + } + + frontTask = nullptr; + data->mTasks.pop(); + // Check if next task could be handled on current thread + if (!data->mTasks.empty() && + data->mTasks.front().second == mCurrentEventTarget) { + frontTask = data->mTasks.front().first; + } + } + } + + MOZ_ASSERT(!frontTask); + + // Post tasks to different thread if pending tasks exist. + { + auto data = mData.Lock(); + data->mCurrentRunnable = nullptr; + mCurrentEventTarget = nullptr; + + if (data->mDestroyed || data->mTasks.empty()) { + return; + } + + PostTasksIfNecessary(data->mTasks.front().second, data); + } +} + +// static +StaticRefPtr<WinWindowOcclusionTracker> WinWindowOcclusionTracker::sTracker; + +/* static */ +WinWindowOcclusionTracker* WinWindowOcclusionTracker::Get() { + MOZ_ASSERT(NS_IsMainThread()); + if (!sTracker || sTracker->mHasAttemptedShutdown) { + return nullptr; + } + return sTracker; +} + +/* static */ +void WinWindowOcclusionTracker::Ensure() { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WinWindowOcclusionTracker::Ensure()"); + + base::Thread::Options options; + options.message_loop_type = MessageLoop::TYPE_UI; + + if (sTracker) { + // Try to reuse the thread, which involves stopping and restarting it. + sTracker->mThread->Stop(); + if (sTracker->mThread->StartWithOptions(options)) { + // Success! + sTracker->mHasAttemptedShutdown = false; + + // Take this opportunity to ensure that mDisplayStatusObserver and + // mSessionChangeObserver exist. They might have failed to be + // created when sTracker was created. + sTracker->EnsureDisplayStatusObserver(); + sTracker->EnsureSessionChangeObserver(); + return; + } + // Restart failed, so null out our sTracker and try again with a new + // thread. This will cause the old singleton instance to be deallocated, + // which will destroy its mThread as well. + sTracker = nullptr; + } + + UniquePtr<base::Thread> thread = + MakeUnique<base::Thread>("WinWindowOcclusionCalc"); + + if (!thread->StartWithOptions(options)) { + return; + } + + sTracker = new WinWindowOcclusionTracker(std::move(thread)); + WindowOcclusionCalculator::CreateInstance(); + + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::Initialize); + sTracker->mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +/* static */ +void WinWindowOcclusionTracker::ShutDown() { + if (!sTracker) { + return; + } + + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WinWindowOcclusionTracker::ShutDown()"); + + sTracker->mHasAttemptedShutdown = true; + sTracker->Destroy(); + + // Our thread could hang while we're waiting for it to stop. + // Since we're shutting down, that's not a critical problem. + // We set a reasonable amount of time to wait for shutdown, + // and if it succeeds within that time, we correctly stop + // our thread by nulling out the refptr, which will cause it + // to be deallocated and join the thread. If it times out, + // we do nothing, which means that the thread will not be + // joined and sTracker memory will leak. + CVStatus status; + { + // It's important to hold the lock before posting the + // runnable. This ensures that the runnable can't begin + // until we've started our Wait, which prevents us from + // Waiting on a monitor that has already been notified. + MonitorAutoLock lock(sTracker->mMonitor); + + static const TimeDuration TIMEOUT = TimeDuration::FromSeconds(2.0); + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::Shutdown); + OcclusionCalculatorLoop()->PostTask(runnable.forget()); + + // Monitor uses SleepConditionVariableSRW, which can have + // spurious wakeups which are reported as timeouts, so we + // check timestamps to ensure that we've waited as long we + // intended to. If we wake early, we don't bother calculating + // a precise amount for the next wait; we just wait the same + // amount of time. This means timeout might happen after as + // much as 2x the TIMEOUT time. + TimeStamp timeStart = TimeStamp::NowLoRes(); + do { + status = sTracker->mMonitor.Wait(TIMEOUT); + } while ((status == CVStatus::Timeout) && + ((TimeStamp::NowLoRes() - timeStart) < TIMEOUT)); + } + + if (status == CVStatus::NoTimeout) { + WindowOcclusionCalculator::ClearInstance(); + sTracker = nullptr; + } +} + +void WinWindowOcclusionTracker::Destroy() { + if (mDisplayStatusObserver) { + mDisplayStatusObserver->Destroy(); + mDisplayStatusObserver = nullptr; + } + if (mSessionChangeObserver) { + mSessionChangeObserver->Destroy(); + mSessionChangeObserver = nullptr; + } + if (mSerializedTaskDispatcher) { + mSerializedTaskDispatcher->Destroy(); + } +} + +/* static */ +MessageLoop* WinWindowOcclusionTracker::OcclusionCalculatorLoop() { + return sTracker ? sTracker->mThread->message_loop() : nullptr; +} + +/* static */ +bool WinWindowOcclusionTracker::IsInWinWindowOcclusionThread() { + return sTracker && + sTracker->mThread->thread_id() == PlatformThread::CurrentId(); +} + +void WinWindowOcclusionTracker::EnsureDisplayStatusObserver() { + if (mDisplayStatusObserver) { + return; + } + if (StaticPrefs:: + widget_windows_window_occlusion_tracking_display_state_enabled()) { + mDisplayStatusObserver = DisplayStatusObserver::Create(this); + } +} + +void WinWindowOcclusionTracker::EnsureSessionChangeObserver() { + if (mSessionChangeObserver) { + return; + } + if (StaticPrefs:: + widget_windows_window_occlusion_tracking_session_lock_enabled()) { + mSessionChangeObserver = SessionChangeObserver::Create(this); + } +} + +void WinWindowOcclusionTracker::Enable(nsBaseWidget* aWindow, HWND aHwnd) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WinWindowOcclusionTracker::Enable() aWindow %p aHwnd %p", + aWindow, aHwnd); + + auto it = mHwndRootWindowMap.find(aHwnd); + if (it != mHwndRootWindowMap.end()) { + return; + } + + nsWeakPtr weak = do_GetWeakReference(aWindow); + mHwndRootWindowMap.emplace(aHwnd, weak); + + RefPtr<Runnable> runnable = WrapRunnable( + RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::EnableOcclusionTrackingForWindow, aHwnd); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +void WinWindowOcclusionTracker::Disable(nsBaseWidget* aWindow, HWND aHwnd) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::Disable() aWindow %p aHwnd %p", aWindow, + aHwnd); + + auto it = mHwndRootWindowMap.find(aHwnd); + if (it == mHwndRootWindowMap.end()) { + return; + } + + mHwndRootWindowMap.erase(it); + + RefPtr<Runnable> runnable = WrapRunnable( + RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::DisableOcclusionTrackingForWindow, aHwnd); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +void WinWindowOcclusionTracker::OnWindowVisibilityChanged(nsBaseWidget* aWindow, + bool aVisible) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::OnWindowVisibilityChanged() aWindow %p " + "aVisible %d", + aWindow, aVisible); + + RefPtr<Runnable> runnable = WrapRunnable( + RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::HandleVisibilityChanged, aVisible); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +WinWindowOcclusionTracker::WinWindowOcclusionTracker( + UniquePtr<base::Thread> aThread) + : mThread(std::move(aThread)), mMonitor("WinWindowOcclusionTracker") { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WinWindowOcclusionTracker::WinWindowOcclusionTracker()"); + + EnsureDisplayStatusObserver(); + EnsureSessionChangeObserver(); + + mSerializedTaskDispatcher = new SerializedTaskDispatcher(); +} + +WinWindowOcclusionTracker::~WinWindowOcclusionTracker() { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::~WinWindowOcclusionTracker()"); +} + +// static +bool WinWindowOcclusionTracker::IsWindowVisibleAndFullyOpaque( + HWND aHwnd, LayoutDeviceIntRect* aWindowRect) { + // Filter out windows that are not "visible", IsWindowVisible(). + if (!::IsWindow(aHwnd) || !::IsWindowVisible(aHwnd)) { + return false; + } + + // Filter out minimized windows. + if (::IsIconic(aHwnd)) { + return false; + } + + LONG exStyles = ::GetWindowLong(aHwnd, GWL_EXSTYLE); + // Filter out "transparent" windows, windows where the mouse clicks fall + // through them. + if (exStyles & WS_EX_TRANSPARENT) { + return false; + } + + // Filter out "tool windows", which are floating windows that do not appear on + // the taskbar or ALT-TAB. Floating windows can have larger window rectangles + // than what is visible to the user, so by filtering them out we will avoid + // incorrectly marking native windows as occluded. We do not filter out the + // Windows Taskbar. + if (exStyles & WS_EX_TOOLWINDOW) { + nsAutoString className; + if (WinUtils::GetClassName(aHwnd, className)) { + if (!className.Equals(L"Shell_TrayWnd")) { + return false; + } + } + } + + // Filter out layered windows that are not opaque or that set a transparency + // colorkey. + if (exStyles & WS_EX_LAYERED) { + BYTE alpha; + DWORD flags; + + // GetLayeredWindowAttributes only works if the application has + // previously called SetLayeredWindowAttributes on the window. + // The function will fail if the layered window was setup with + // UpdateLayeredWindow. Treat this failure as the window being transparent. + // See Remarks section of + // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getlayeredwindowattributes + if (!::GetLayeredWindowAttributes(aHwnd, nullptr, &alpha, &flags)) { + return false; + } + + if (flags & LWA_ALPHA && alpha < 255) { + return false; + } + if (flags & LWA_COLORKEY) { + return false; + } + } + + // Filter out windows that do not have a simple rectangular region. + HRGN region = ::CreateRectRgn(0, 0, 0, 0); + int result = GetWindowRgn(aHwnd, region); + ::DeleteObject(region); + if (result == COMPLEXREGION) { + return false; + } + + // Windows 10 has cloaked windows, windows with WS_VISIBLE attribute but + // not displayed. explorer.exe, in particular has one that's the + // size of the desktop. It's usually behind Chrome windows in the z-order, + // but using a remote desktop can move it up in the z-order. So, ignore them. + DWORD reason; + if (SUCCEEDED(::DwmGetWindowAttribute(aHwnd, DWMWA_CLOAKED, &reason, + sizeof(reason))) && + reason != 0) { + return false; + } + + RECT winRect; + // Filter out windows that take up zero area. The call to GetWindowRect is one + // of the most expensive parts of this function, so it is last. + if (!::GetWindowRect(aHwnd, &winRect)) { + return false; + } + if (::IsRectEmpty(&winRect)) { + return false; + } + + // Ignore popup windows since they're transient unless it is the Windows + // Taskbar + // XXX Chrome Widget popup handling is removed for now. + if (::GetWindowLong(aHwnd, GWL_STYLE) & WS_POPUP) { + nsAutoString className; + if (WinUtils::GetClassName(aHwnd, className)) { + if (!className.Equals(L"Shell_TrayWnd")) { + return false; + } + } + } + + *aWindowRect = LayoutDeviceIntRect(winRect.left, winRect.top, + winRect.right - winRect.left, + winRect.bottom - winRect.top); + + WINDOWPLACEMENT windowPlacement = {0}; + windowPlacement.length = sizeof(WINDOWPLACEMENT); + ::GetWindowPlacement(aHwnd, &windowPlacement); + if (windowPlacement.showCmd == SW_MAXIMIZE) { + // If the window is maximized the window border extends beyond the visible + // region of the screen. Adjust the maximized window rect to fit the + // screen dimensions to ensure that fullscreen windows, which do not extend + // beyond the screen boundaries since they typically have no borders, will + // occlude maximized windows underneath them. + HMONITOR hmon = ::MonitorFromWindow(aHwnd, MONITOR_DEFAULTTONEAREST); + if (hmon) { + MONITORINFO mi; + mi.cbSize = sizeof(mi); + if (GetMonitorInfo(hmon, &mi)) { + LayoutDeviceIntRect workArea(mi.rcWork.left, mi.rcWork.top, + mi.rcWork.right - mi.rcWork.left, + mi.rcWork.bottom - mi.rcWork.top); + // Adjust aWindowRect to fit to monitor. + aWindowRect->width = std::min(workArea.width, aWindowRect->width); + if (aWindowRect->x < workArea.x) { + aWindowRect->x = workArea.x; + } else { + aWindowRect->x = std::min(workArea.x + workArea.width, + aWindowRect->x + aWindowRect->width) - + aWindowRect->width; + } + aWindowRect->height = std::min(workArea.height, aWindowRect->height); + if (aWindowRect->y < workArea.y) { + aWindowRect->y = workArea.y; + } else { + aWindowRect->y = std::min(workArea.y + workArea.height, + aWindowRect->y + aWindowRect->height) - + aWindowRect->height; + } + } + } + } + + return true; +} + +// static +void WinWindowOcclusionTracker::CallUpdateOcclusionState( + std::unordered_map<HWND, OcclusionState>* aMap, bool aShowAllWindows) { + MOZ_ASSERT(NS_IsMainThread()); + + auto* tracker = WinWindowOcclusionTracker::Get(); + if (!tracker) { + return; + } + tracker->UpdateOcclusionState(aMap, aShowAllWindows); +} + +void WinWindowOcclusionTracker::UpdateOcclusionState( + std::unordered_map<HWND, OcclusionState>* aMap, bool aShowAllWindows) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread()); + LOG(LogLevel::Debug, + "WinWindowOcclusionTracker::UpdateOcclusionState() aShowAllWindows %d", + aShowAllWindows); + + mNumVisibleRootWindows = 0; + for (auto& [hwnd, state] : *aMap) { + auto it = mHwndRootWindowMap.find(hwnd); + // The window was destroyed while processing occlusion. + if (it == mHwndRootWindowMap.end()) { + continue; + } + auto occlState = state; + + // If the screen is locked or off, ignore occlusion state results and + // mark the window as occluded. + if (mScreenLocked || !mDisplayOn) { + occlState = OcclusionState::OCCLUDED; + } else if (aShowAllWindows) { + occlState = OcclusionState::VISIBLE; + } + nsCOMPtr<nsIWidget> widget = do_QueryReferent(it->second); + if (!widget) { + continue; + } + auto* baseWidget = static_cast<nsBaseWidget*>(widget.get()); + baseWidget->NotifyOcclusionState(occlState); + if (baseWidget->SizeMode() != nsSizeMode_Minimized) { + mNumVisibleRootWindows++; + } + } +} + +void WinWindowOcclusionTracker::OnSessionChange(WPARAM aStatusCode, + Maybe<bool> aIsCurrentSession) { + MOZ_ASSERT(NS_IsMainThread()); + + if (aIsCurrentSession.isNothing() || !*aIsCurrentSession) { + return; + } + + if (aStatusCode == WTS_SESSION_UNLOCK) { + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::OnSessionChange() WTS_SESSION_UNLOCK"); + + // UNLOCK will cause a foreground window change, which will + // trigger an occlusion calculation on its own. + mScreenLocked = false; + } else if (aStatusCode == WTS_SESSION_LOCK) { + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::OnSessionChange() WTS_SESSION_LOCK"); + + mScreenLocked = true; + MarkNonIconicWindowsOccluded(); + } +} + +void WinWindowOcclusionTracker::OnDisplayStateChanged(bool aDisplayOn) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::OnDisplayStateChanged() aDisplayOn %d", + aDisplayOn); + + if (mDisplayOn == aDisplayOn) { + return; + } + + mDisplayOn = aDisplayOn; + if (aDisplayOn) { + // Notify the window occlusion calculator of the display turning on + // which will schedule an occlusion calculation. This must be run + // on the WindowOcclusionCalculator thread. + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::HandleVisibilityChanged, + /* aVisible */ true); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); + } else { + MarkNonIconicWindowsOccluded(); + } +} + +void WinWindowOcclusionTracker::MarkNonIconicWindowsOccluded() { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, + "WinWindowOcclusionTracker::MarkNonIconicWindowsOccluded()"); + + // Set all visible root windows as occluded. If not visible, + // set them as hidden. + for (auto& [hwnd, weak] : mHwndRootWindowMap) { + nsCOMPtr<nsIWidget> widget = do_QueryReferent(weak); + if (!widget) { + continue; + } + auto* baseWidget = static_cast<nsBaseWidget*>(widget.get()); + auto state = (baseWidget->SizeMode() == nsSizeMode_Minimized) + ? OcclusionState::HIDDEN + : OcclusionState::OCCLUDED; + baseWidget->NotifyOcclusionState(state); + } +} + +void WinWindowOcclusionTracker::TriggerCalculation() { + RefPtr<Runnable> runnable = + WrapRunnable(RefPtr<WindowOcclusionCalculator>( + WindowOcclusionCalculator::GetInstance()), + &WindowOcclusionCalculator::HandleTriggerCalculation); + mSerializedTaskDispatcher->PostTaskToCalculator(runnable.forget()); +} + +// static +BOOL WinWindowOcclusionTracker::DumpOccludingWindowsCallback(HWND aHWnd, + LPARAM aLParam) { + HWND hwnd = reinterpret_cast<HWND>(aLParam); + + LayoutDeviceIntRect windowRect; + bool windowIsOccluding = IsWindowVisibleAndFullyOpaque(aHWnd, &windowRect); + if (windowIsOccluding) { + nsAutoString className; + if (WinUtils::GetClassName(aHWnd, className)) { + const auto name = NS_ConvertUTF16toUTF8(className); + printf_stderr( + "DumpOccludingWindowsCallback() aHWnd %p className %s windowRect(%d, " + "%d, %d, %d)\n", + aHWnd, name.get(), windowRect.x, windowRect.y, windowRect.width, + windowRect.height); + } + } + + if (aHWnd == hwnd) { + return false; + } + return true; +} + +void WinWindowOcclusionTracker::DumpOccludingWindows(HWND aHWnd) { + printf_stderr("DumpOccludingWindows() until aHWnd %p visible %d iconic %d\n", + aHWnd, ::IsWindowVisible(aHWnd), ::IsIconic(aHWnd)); + ::EnumWindows(&DumpOccludingWindowsCallback, reinterpret_cast<LPARAM>(aHWnd)); +} + +// static +StaticRefPtr<WinWindowOcclusionTracker::WindowOcclusionCalculator> + WinWindowOcclusionTracker::WindowOcclusionCalculator::sCalculator; + +WinWindowOcclusionTracker::WindowOcclusionCalculator:: + WindowOcclusionCalculator() + : mMonitor(WinWindowOcclusionTracker::Get()->mMonitor) { + MOZ_ASSERT(NS_IsMainThread()); + LOG(LogLevel::Info, "WindowOcclusionCalculator()"); + + mSerializedTaskDispatcher = + WinWindowOcclusionTracker::Get()->GetSerializedTaskDispatcher(); +} + +WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ~WindowOcclusionCalculator() {} + +// static +void WinWindowOcclusionTracker::WindowOcclusionCalculator::CreateInstance() { + MOZ_ASSERT(NS_IsMainThread()); + sCalculator = new WindowOcclusionCalculator(); +} + +// static +void WinWindowOcclusionTracker::WindowOcclusionCalculator::ClearInstance() { + MOZ_ASSERT(NS_IsMainThread()); + sCalculator = nullptr; +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator::Initialize() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_ASSERT(!mVirtualDesktopManager); + CALC_LOG(LogLevel::Info, "Initialize()"); + +#ifndef __MINGW32__ + if (!IsWin10OrLater()) { + return; + } + + RefPtr<IVirtualDesktopManager> desktopManager; + HRESULT hr = ::CoCreateInstance( + CLSID_VirtualDesktopManager, NULL, CLSCTX_INPROC_SERVER, + __uuidof(IVirtualDesktopManager), getter_AddRefs(desktopManager)); + if (FAILED(hr)) { + return; + } + mVirtualDesktopManager = desktopManager; +#endif +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator::Shutdown() { + MonitorAutoLock lock(mMonitor); + + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Info, "Shutdown()"); + + UnregisterEventHooks(); + if (mOcclusionUpdateRunnable) { + mOcclusionUpdateRunnable->Cancel(); + mOcclusionUpdateRunnable = nullptr; + } + mVirtualDesktopManager = nullptr; + + mMonitor.NotifyAll(); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + EnableOcclusionTrackingForWindow(HWND aHwnd) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread()); + CALC_LOG(LogLevel::Info, "EnableOcclusionTrackingForWindow() aHwnd %p", + aHwnd); + + MOZ_RELEASE_ASSERT(mRootWindowHwndsOcclusionState.find(aHwnd) == + mRootWindowHwndsOcclusionState.end()); + mRootWindowHwndsOcclusionState[aHwnd] = OcclusionState::UNKNOWN; + + if (mGlobalEventHooks.empty()) { + RegisterEventHooks(); + } + + // Schedule an occlusion calculation so that the newly tracked window does + // not have a stale occlusion status. + ScheduleOcclusionCalculationIfNeeded(); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + DisableOcclusionTrackingForWindow(HWND aHwnd) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread()); + CALC_LOG(LogLevel::Info, "DisableOcclusionTrackingForWindow() aHwnd %p", + aHwnd); + + MOZ_RELEASE_ASSERT(mRootWindowHwndsOcclusionState.find(aHwnd) != + mRootWindowHwndsOcclusionState.end()); + mRootWindowHwndsOcclusionState.erase(aHwnd); + + if (mMovingWindow == aHwnd) { + mMovingWindow = 0; + } + + if (mRootWindowHwndsOcclusionState.empty()) { + UnregisterEventHooks(); + if (mOcclusionUpdateRunnable) { + mOcclusionUpdateRunnable->Cancel(); + mOcclusionUpdateRunnable = nullptr; + } + } +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + HandleVisibilityChanged(bool aVisible) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Info, "HandleVisibilityChange() aVisible %d", aVisible); + + // May have gone from having no visible windows to having one, in + // which case we need to register event hooks, and make sure that an + // occlusion calculation is scheduled. + if (aVisible) { + MaybeRegisterEventHooks(); + ScheduleOcclusionCalculationIfNeeded(); + } +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + HandleTriggerCalculation() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Info, "HandleTriggerCalculation()"); + + MaybeRegisterEventHooks(); + ScheduleOcclusionCalculationIfNeeded(); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + MaybeRegisterEventHooks() { + if (mGlobalEventHooks.empty()) { + RegisterEventHooks(); + } +} + +// static +void CALLBACK +WinWindowOcclusionTracker::WindowOcclusionCalculator::EventHookCallback( + HWINEVENTHOOK aWinEventHook, DWORD aEvent, HWND aHwnd, LONG aIdObject, + LONG aIdChild, DWORD aEventThread, DWORD aMsEventTime) { + if (sCalculator) { + sCalculator->ProcessEventHookCallback(aWinEventHook, aEvent, aHwnd, + aIdObject, aIdChild); + } +} + +// static +BOOL CALLBACK WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ComputeNativeWindowOcclusionStatusCallback(HWND aHwnd, LPARAM aLParam) { + if (sCalculator) { + return sCalculator->ProcessComputeNativeWindowOcclusionStatusCallback( + aHwnd, reinterpret_cast<std::unordered_set<DWORD>*>(aLParam)); + } + return FALSE; +} + +// static +BOOL CALLBACK WinWindowOcclusionTracker::WindowOcclusionCalculator:: + UpdateVisibleWindowProcessIdsCallback(HWND aHwnd, LPARAM aLParam) { + if (sCalculator) { + sCalculator->ProcessUpdateVisibleWindowProcessIdsCallback(aHwnd); + return TRUE; + } + return FALSE; +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + UpdateVisibleWindowProcessIds() { + mPidsForLocationChangeHook.clear(); + ::EnumWindows(&UpdateVisibleWindowProcessIdsCallback, 0); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ComputeNativeWindowOcclusionStatus() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_ASSERT(mSerializedTaskDispatcher->IsOnCurrentThread()); + + if (mOcclusionUpdateRunnable) { + mOcclusionUpdateRunnable = nullptr; + } + + if (mRootWindowHwndsOcclusionState.empty()) { + return; + } + + // Set up initial conditions for occlusion calculation. + bool shouldUnregisterEventHooks = true; + + // Compute the LayoutDeviceIntRegion for the screen. + int screenLeft = ::GetSystemMetrics(SM_XVIRTUALSCREEN); + int screenTop = ::GetSystemMetrics(SM_YVIRTUALSCREEN); + int screenWidth = ::GetSystemMetrics(SM_CXVIRTUALSCREEN); + int screenHeight = ::GetSystemMetrics(SM_CYVIRTUALSCREEN); + LayoutDeviceIntRegion screenRegion = + LayoutDeviceIntRect(screenLeft, screenTop, screenWidth, screenHeight); + mNumRootWindowsWithUnknownOcclusionState = 0; + + CALC_LOG(LogLevel::Debug, + "ComputeNativeWindowOcclusionStatus() screen(%d, %d, %d, %d)", + screenLeft, screenTop, screenWidth, screenHeight); + + for (auto& [hwnd, state] : mRootWindowHwndsOcclusionState) { + // IsIconic() checks for a minimized window. Immediately set the state of + // minimized windows to HIDDEN. + if (::IsIconic(hwnd)) { + state = OcclusionState::HIDDEN; + } else if (IsWindowOnCurrentVirtualDesktop(hwnd) == Some(false)) { + // If window is not on the current virtual desktop, immediately + // set the state of the window to OCCLUDED. + state = OcclusionState::OCCLUDED; + // Don't unregister event hooks when not on current desktop. There's no + // notification when that changes, so we can't reregister event hooks. + shouldUnregisterEventHooks = false; + } else { + state = OcclusionState::UNKNOWN; + shouldUnregisterEventHooks = false; + mNumRootWindowsWithUnknownOcclusionState++; + } + } + + // Unregister event hooks if all native windows are minimized. + if (shouldUnregisterEventHooks) { + UnregisterEventHooks(); + } else { + std::unordered_set<DWORD> currentPidsWithVisibleWindows; + mUnoccludedDesktopRegion = screenRegion; + // Calculate unoccluded region if there is a non-minimized native window. + // Also compute |current_pids_with_visible_windows| as we enumerate + // the windows. + EnumWindows(&ComputeNativeWindowOcclusionStatusCallback, + reinterpret_cast<LPARAM>(¤tPidsWithVisibleWindows)); + // Check if mPidsForLocationChangeHook has any pids of processes + // currently without visible windows. If so, unhook the win event, + // remove the pid from mPidsForLocationChangeHook and remove + // the corresponding event hook from mProcessEventHooks. + std::unordered_set<DWORD> pidsToRemove; + for (auto locChangePid : mPidsForLocationChangeHook) { + if (currentPidsWithVisibleWindows.find(locChangePid) == + currentPidsWithVisibleWindows.end()) { + // Remove the event hook from our map, and unregister the event hook. + // It's possible the eventhook will no longer be valid, but if we don't + // unregister the event hook, a process that toggles between having + // visible windows and not having visible windows could cause duplicate + // event hooks to get registered for the process. + UnhookWinEvent(mProcessEventHooks[locChangePid]); + mProcessEventHooks.erase(locChangePid); + pidsToRemove.insert(locChangePid); + } + } + if (!pidsToRemove.empty()) { + // XXX simplify + for (auto it = mPidsForLocationChangeHook.begin(); + it != mPidsForLocationChangeHook.end();) { + if (pidsToRemove.find(*it) != pidsToRemove.end()) { + it = mPidsForLocationChangeHook.erase(it); + } else { + ++it; + } + } + } + } + + std::unordered_map<HWND, OcclusionState>* map = + &mRootWindowHwndsOcclusionState; + bool showAllWindows = mShowingThumbnails; + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "CallUpdateOcclusionState", [map, showAllWindows]() { + WinWindowOcclusionTracker::CallUpdateOcclusionState(map, + showAllWindows); + }); + mSerializedTaskDispatcher->PostTaskToMain(runnable.forget()); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ScheduleOcclusionCalculationIfNeeded() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + + // OcclusionUpdateRunnable is already queued. + if (mOcclusionUpdateRunnable) { + return; + } + + CALC_LOG(LogLevel::Debug, "ScheduleOcclusionCalculationIfNeeded()"); + + RefPtr<CancelableRunnable> task = new OcclusionUpdateRunnable(this); + mOcclusionUpdateRunnable = task; + mSerializedTaskDispatcher->PostDelayedTaskToCalculator( + task.forget(), kOcclusionUpdateRunnableDelayMs); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + RegisterGlobalEventHook(DWORD aEventMin, DWORD aEventMax) { + HWINEVENTHOOK eventHook = + ::SetWinEventHook(aEventMin, aEventMax, nullptr, &EventHookCallback, 0, 0, + WINEVENT_OUTOFCONTEXT); + mGlobalEventHooks.push_back(eventHook); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + RegisterEventHookForProcess(DWORD aPid) { + mPidsForLocationChangeHook.insert(aPid); + mProcessEventHooks[aPid] = SetWinEventHook( + EVENT_OBJECT_LOCATIONCHANGE, EVENT_OBJECT_LOCATIONCHANGE, nullptr, + &EventHookCallback, aPid, 0, WINEVENT_OUTOFCONTEXT); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + RegisterEventHooks() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + MOZ_RELEASE_ASSERT(mGlobalEventHooks.empty()); + CALC_LOG(LogLevel::Info, "RegisterEventHooks()"); + + // Detects native window lost mouse capture + RegisterGlobalEventHook(EVENT_SYSTEM_CAPTUREEND, EVENT_SYSTEM_CAPTUREEND); + + // Detects native window move (drag) and resizing events. + RegisterGlobalEventHook(EVENT_SYSTEM_MOVESIZESTART, EVENT_SYSTEM_MOVESIZEEND); + + // Detects native window minimize and restore from taskbar events. + RegisterGlobalEventHook(EVENT_SYSTEM_MINIMIZESTART, EVENT_SYSTEM_MINIMIZEEND); + + // Detects foreground window changing. + RegisterGlobalEventHook(EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND); + + // Detects objects getting shown and hidden. Used to know when the task bar + // and alt tab are showing preview windows so we can unocclude windows. + RegisterGlobalEventHook(EVENT_OBJECT_SHOW, EVENT_OBJECT_HIDE); + + // Detects object state changes, e.g., enable/disable state, native window + // maximize and native window restore events. + RegisterGlobalEventHook(EVENT_OBJECT_STATECHANGE, EVENT_OBJECT_STATECHANGE); + + // Cloaking and uncloaking of windows should trigger an occlusion calculation. + // In particular, switching virtual desktops seems to generate these events. + RegisterGlobalEventHook(EVENT_OBJECT_CLOAKED, EVENT_OBJECT_UNCLOAKED); + + // Determine which subset of processes to set EVENT_OBJECT_LOCATIONCHANGE on + // because otherwise event throughput is very high, as it generates events + // for location changes of all objects, including the mouse moving on top of a + // window. + UpdateVisibleWindowProcessIds(); + for (DWORD pid : mPidsForLocationChangeHook) { + RegisterEventHookForProcess(pid); + } +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + UnregisterEventHooks() { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + CALC_LOG(LogLevel::Info, "UnregisterEventHooks()"); + + for (const auto eventHook : mGlobalEventHooks) { + ::UnhookWinEvent(eventHook); + } + mGlobalEventHooks.clear(); + + for (const auto& [pid, eventHook] : mProcessEventHooks) { + ::UnhookWinEvent(eventHook); + } + mProcessEventHooks.clear(); + + mPidsForLocationChangeHook.clear(); +} + +bool WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ProcessComputeNativeWindowOcclusionStatusCallback( + HWND aHwnd, std::unordered_set<DWORD>* aCurrentPidsWithVisibleWindows) { + LayoutDeviceIntRegion currUnoccludedDestkop = mUnoccludedDesktopRegion; + LayoutDeviceIntRect windowRect; + bool windowIsOccluding = + WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(aHwnd, &windowRect); + if (windowIsOccluding) { + // Hook this window's process with EVENT_OBJECT_LOCATION_CHANGE, if we are + // not already doing so. + DWORD pid; + ::GetWindowThreadProcessId(aHwnd, &pid); + aCurrentPidsWithVisibleWindows->insert(pid); + auto it = mProcessEventHooks.find(pid); + if (it == mProcessEventHooks.end()) { + RegisterEventHookForProcess(pid); + } + + // If no more root windows to consider, return true so we can continue + // looking for windows we haven't hooked. + if (mNumRootWindowsWithUnknownOcclusionState == 0) { + return true; + } + + mUnoccludedDesktopRegion.SubOut(windowRect); + } else if (mNumRootWindowsWithUnknownOcclusionState == 0) { + // This window can't occlude other windows, but we've determined the + // occlusion state of all root windows, so we can return. + return true; + } + + // Ignore moving windows when deciding if windows under it are occluded. + if (aHwnd == mMovingWindow) { + return true; + } + + // Check if |hwnd| is a root window; if so, we're done figuring out + // if it's occluded because we've seen all the windows "over" it. + auto it = mRootWindowHwndsOcclusionState.find(aHwnd); + if (it == mRootWindowHwndsOcclusionState.end() || + it->second != OcclusionState::UNKNOWN) { + return true; + } + + CALC_LOG(LogLevel::Debug, + "ProcessComputeNativeWindowOcclusionStatusCallback() windowRect(%d, " + "%d, %d, %d) IsOccluding %d", + windowRect.x, windowRect.y, windowRect.width, windowRect.height, + windowIsOccluding); + + // On Win7, default theme makes root windows have complex regions by + // default. But we can still check if their bounding rect is occluded. + if (!windowIsOccluding) { + RECT rect; + if (::GetWindowRect(aHwnd, &rect) != 0) { + LayoutDeviceIntRect windowRect( + rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top); + currUnoccludedDestkop.SubOut(windowRect); + } + } + + it->second = (mUnoccludedDesktopRegion == currUnoccludedDestkop) + ? OcclusionState::OCCLUDED + : OcclusionState::VISIBLE; + mNumRootWindowsWithUnknownOcclusionState--; + + return true; +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ProcessEventHookCallback(HWINEVENTHOOK aWinEventHook, DWORD aEvent, + HWND aHwnd, LONG aIdObject, LONG aIdChild) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + + // No need to calculate occlusion if a zero HWND generated the event. This + // happens if there is no window associated with the event, e.g., mouse move + // events. + if (!aHwnd) { + return; + } + + // We only care about events for window objects. In particular, we don't care + // about OBJID_CARET, which is spammy. + if (aIdObject != OBJID_WINDOW) { + return; + } + + CALC_LOG(LogLevel::Debug, + "WindowOcclusionCalculator::ProcessEventHookCallback() aEvent 0x%lx", + aEvent); + + // We generally ignore events for popup windows, except for when the taskbar + // is hidden or Windows Taskbar, in which case we recalculate occlusion. + // XXX Chrome Widget popup handling is removed for now. + bool calculateOcclusion = true; + if (::GetWindowLong(aHwnd, GWL_STYLE) & WS_POPUP) { + nsAutoString className; + if (WinUtils::GetClassName(aHwnd, className)) { + calculateOcclusion = className.Equals(L"Shell_TrayWnd"); + } + } + + // Detect if either the alt tab view or the task list thumbnail is being + // shown. If so, mark all non-hidden windows as occluded, and remember that + // we're in the showing_thumbnails state. This lasts until we get told that + // either the alt tab view or task list thumbnail are hidden. + if (aEvent == EVENT_OBJECT_SHOW) { + // Avoid getting the aHwnd's class name, and recomputing occlusion, if not + // needed. + if (mShowingThumbnails) { + return; + } + nsAutoString className; + if (WinUtils::GetClassName(aHwnd, className)) { + const auto name = NS_ConvertUTF16toUTF8(className); + CALC_LOG(LogLevel::Debug, + "ProcessEventHookCallback() EVENT_OBJECT_SHOW %s", name.get()); + + if (name.Equals("MultitaskingViewFrame") || + name.Equals("TaskListThumbnailWnd")) { + CALC_LOG(LogLevel::Info, + "ProcessEventHookCallback() mShowingThumbnails = true"); + mShowingThumbnails = true; + + std::unordered_map<HWND, OcclusionState>* map = + &mRootWindowHwndsOcclusionState; + bool showAllWindows = mShowingThumbnails; + RefPtr<Runnable> runnable = NS_NewRunnableFunction( + "CallUpdateOcclusionState", [map, showAllWindows]() { + WinWindowOcclusionTracker::CallUpdateOcclusionState( + map, showAllWindows); + }); + mSerializedTaskDispatcher->PostTaskToMain(runnable.forget()); + } + } + return; + } else if (aEvent == EVENT_OBJECT_HIDE) { + // Avoid getting the aHwnd's class name, and recomputing occlusion, if not + // needed. + if (!mShowingThumbnails) { + return; + } + nsAutoString className; + WinUtils::GetClassName(aHwnd, className); + const auto name = NS_ConvertUTF16toUTF8(className); + CALC_LOG(LogLevel::Debug, "ProcessEventHookCallback() EVENT_OBJECT_HIDE %s", + name.get()); + if (name.Equals("MultitaskingViewFrame") || + name.Equals("TaskListThumbnailWnd")) { + CALC_LOG(LogLevel::Info, + "ProcessEventHookCallback() mShowingThumbnails = false"); + mShowingThumbnails = false; + // Let occlusion calculation fix occlusion state, even though hwnd might + // be a popup window. + calculateOcclusion = true; + } else { + return; + } + } + // Don't continually calculate occlusion while a window is moving (unless it's + // a root window), but instead once at the beginning and once at the end. + // Remember the window being moved so if it's a root window, we can ignore + // it when deciding if windows under it are occluded. + else if (aEvent == EVENT_SYSTEM_MOVESIZESTART) { + mMovingWindow = aHwnd; + } else if (aEvent == EVENT_SYSTEM_MOVESIZEEND) { + mMovingWindow = 0; + } else if (mMovingWindow != 0) { + if (aEvent == EVENT_OBJECT_LOCATIONCHANGE || + aEvent == EVENT_OBJECT_STATECHANGE) { + // Ignore move events if it's not a root window that's being moved. If it + // is a root window, we want to calculate occlusion to support tab + // dragging to windows that were occluded when the drag was started but + // are no longer occluded. + if (mRootWindowHwndsOcclusionState.find(aHwnd) == + mRootWindowHwndsOcclusionState.end()) { + return; + } + } else { + // If we get an event that isn't a location/state change, then we probably + // missed the movesizeend notification, or got events out of order. In + // that case, we want to go back to normal occlusion calculation. + mMovingWindow = 0; + } + } + + if (!calculateOcclusion) { + return; + } + + ScheduleOcclusionCalculationIfNeeded(); +} + +void WinWindowOcclusionTracker::WindowOcclusionCalculator:: + ProcessUpdateVisibleWindowProcessIdsCallback(HWND aHwnd) { + MOZ_ASSERT(IsInWinWindowOcclusionThread()); + + LayoutDeviceIntRect windowRect; + if (WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop(aHwnd, &windowRect)) { + DWORD pid; + ::GetWindowThreadProcessId(aHwnd, &pid); + mPidsForLocationChangeHook.insert(pid); + } +} + +bool WinWindowOcclusionTracker::WindowOcclusionCalculator:: + WindowCanOccludeOtherWindowsOnCurrentVirtualDesktop( + HWND aHwnd, LayoutDeviceIntRect* aWindowRect) { + return IsWindowVisibleAndFullyOpaque(aHwnd, aWindowRect) && + (IsWindowOnCurrentVirtualDesktop(aHwnd) == Some(true)); +} + +Maybe<bool> WinWindowOcclusionTracker::WindowOcclusionCalculator:: + IsWindowOnCurrentVirtualDesktop(HWND aHwnd) { + if (!mVirtualDesktopManager) { + return Some(true); + } + + BOOL onCurrentDesktop; + HRESULT hr = mVirtualDesktopManager->IsWindowOnCurrentVirtualDesktop( + aHwnd, &onCurrentDesktop); + if (FAILED(hr)) { + // In this case, we do not know the window is in which virtual desktop. + return Nothing(); + } + + if (onCurrentDesktop) { + return Some(true); + } + + GUID workspaceGuid; + hr = mVirtualDesktopManager->GetWindowDesktopId(aHwnd, &workspaceGuid); + if (FAILED(hr)) { + // In this case, we do not know the window is in which virtual desktop. + return Nothing(); + } + + // IsWindowOnCurrentVirtualDesktop() is flaky for newly opened windows, + // which causes test flakiness. Occasionally, it incorrectly says a window + // is not on the current virtual desktop when it is. In this situation, + // it also returns GUID_NULL for the desktop id. + if (workspaceGuid == GUID_NULL) { + // In this case, we do not know if the window is in which virtual desktop. + // But we hanle it as on current virtual desktop. + // It does not cause a problem to window occlusion. + // Since if window is not on current virtual desktop, window size becomes + // (0, 0, 0, 0). It makes window occlusion handling explicit. It is + // necessary for gtest. + return Some(true); + } + + return Some(false); +} + +#undef LOG +#undef CALC_LOG + +} // namespace mozilla::widget |