Leonard Hecker a25d968fe0
Move ConPTY handoff logic into WindowEmperor (#19088)
This changes the ConPTY handoff COM server from `REGCLS_SINGLEUSE`
to `REGCLS_MULTIPLEUSE`. The former causes a race condition, because
handoff runs concurrently with the creation of WinUI windows.
This can then result in the a window getting the wrong handoff.

It then moves the "root" of ConPTY handoff from `TerminalPage`
(WindowEmperor -> AppHost -> TerminalWindow -> TerminalPage)
into `WindowEmperor` (WindowEmperor).

Closes #19049

## Validation Steps Performed
* Launching cmd from the Start Menu shows a "Command Prompt" tab 
* Win+R -> `cmd` creates windows in the foreground 
* Win+R -> `cmd /c start /max cmd` creates a fullscreen tab 
  * This even works for multiple windows, unlike with Canary 
* Win+R -> `cmd /c start /min cmd` does not work 
  * It also doesn't work in Canary, so it's not a bug in this PR 
2025-07-01 19:00:00 +00:00

1880 lines
70 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "IslandWindow.h"
#include "../types/inc/Viewport.hpp"
#include "resource.h"
#include "icon.h"
#include <dwmapi.h>
#include <TerminalThemeHelpers.h>
#include <CoreWindow.h>
extern "C" IMAGE_DOS_HEADER __ImageBase;
using namespace winrt::Windows::UI;
using namespace winrt::Windows::UI::Composition;
using namespace winrt::Windows::UI::Xaml;
using namespace winrt::Windows::UI::Xaml::Hosting;
using namespace winrt::Windows::Foundation::Numerics;
using namespace winrt::Microsoft::Terminal::Settings::Model;
using namespace winrt::Microsoft::Terminal::Control;
using namespace winrt::Microsoft::Terminal;
using namespace ::Microsoft::Console::Types;
using VirtualKeyModifiers = winrt::Windows::System::VirtualKeyModifiers;
#define XAML_HOSTING_WINDOW_CLASS_NAME L"CASCADIA_HOSTING_WINDOW_CLASS"
#define IDM_SYSTEM_MENU_BEGIN 0x1000
// WinUI doesn't support the "Hide cursor on input" setting which is why all the modern Windows apps
// are broken in that respect. We want to support it though, so we implement an imitation of it here.
// We use the classic ShowCursor() API to hide it on keydown and show it on a select few messages.
// WinUI's SetPointerCapture() cannot be used for this, because it races with internal WinUI code
// calling that function and has proven itself to be very unreliable in practice.
//
// With UWP half the input stack got split off, and so most input events get rerouted through
// the CoreInput child window running in another thread (aka InputHost aka Windows.UI.Input).
// HideCursor() is called by WindowEmperor because WM_KEYDOWN is otherwise sent directly to that
// CoreInput window. Same for WM_POINTERUPDATE which we use to reliably detect cursor movement.
// WM_ACTIVATE on the other hand is only sent to each specific window and cannot be hooked by
// inspecting the MSG struct coming from GetMessage. That's why the code must be here.
// Best not think about this too much...
bool IslandWindow::IsCursorHidden() noexcept
{
return _cursorHidden;
}
void IslandWindow::HideCursor() noexcept
{
static const auto shouldVanish = []() noexcept {
BOOL shouldVanish = TRUE;
SystemParametersInfoW(SPI_GETMOUSEVANISH, 0, &shouldVanish, 0);
return shouldVanish != FALSE;
}();
if (!_cursorHidden && shouldVanish)
{
ShowCursor(FALSE);
_cursorHidden = true;
}
}
void IslandWindow::ShowCursorMaybe(const UINT message) noexcept
{
if (_cursorHidden &&
(message == WM_ACTIVATE ||
message == WM_POINTERUPDATE ||
message == WM_NCPOINTERUPDATE))
{
_cursorHidden = false;
ShowCursor(TRUE);
}
}
IslandWindow::IslandWindow() noexcept :
_interopWindowHandle{ nullptr },
_rootGrid{ nullptr },
_source{ nullptr },
_pfnCreateCallback{ nullptr }
{
}
IslandWindow::~IslandWindow()
{
Close();
}
void IslandWindow::Close()
{
// GH#15454: Unset the user data for the window. This will prevent future
// callbacks that come onto our window message loop from being sent to the
// IslandWindow (or other derived class's) implementation.
//
// Specifically, this prevents a pending coroutine from being able to call
// something like ShowWindow, and have that come back on the IslandWindow
// message loop, where it'll end up asking XAML something that XAML is no
// longer able to answer.
SetWindowLongPtr(_window.get(), GWLP_USERDATA, 0);
if (_source)
{
// BODGY
// WinUI will strongly hold onto the first DesktopWindowXamlSource that is created.
// If we don't manually set the Content() to null first, closing that first window
// will leak all of its contents permanently.
_source.Content(nullptr);
_source.Close();
_source = nullptr;
}
}
HWND IslandWindow::GetInteropHandle() const
{
return _interopWindowHandle;
}
// Method Description:
// - Create the actual window that we'll use for the application.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::MakeWindow() noexcept
{
if (_window)
{
// no-op if we already have a window.
return;
}
WNDCLASS wc{};
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hInstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
wc.lpszClassName = XAML_HOSTING_WINDOW_CLASS_NAME;
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.hIcon = LoadIconW(wc.hInstance, MAKEINTRESOURCEW(IDI_APPICON));
RegisterClass(&wc);
WINRT_ASSERT(!_window);
// Create the window with the default size here - During the creation of the
// window, the system will give us a chance to set its size in WM_CREATE.
// WM_CREATE will be handled synchronously, before CreateWindow returns.
//
// We need WS_EX_NOREDIRECTIONBITMAP for vintage style opacity, GH#603
//
// WS_EX_LAYERED acts REAL WEIRD with TerminalTrySetTransparentBackground,
// but it works just fine when the window is in the TOPMOST group. But if
// you enable it always, activating the window will remove our DWM frame
// entirely. Weird.
WINRT_VERIFY(CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP | (_alwaysOnTop ? WS_EX_TOPMOST : 0),
wc.lpszClassName,
L"Windows Terminal",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
nullptr,
nullptr,
wc.hInstance,
this));
WINRT_ASSERT(_window);
}
// Method Description:
// - Set a callback to be called when we process a WM_CREATE message. This gives
// the AppHost a chance to resize the window to the proper size.
// Arguments:
// - pfn: a function to be called during the handling of WM_CREATE. It takes two
// parameters:
// * HWND: the HWND of the window that's being created.
// * til::rect: The position on the screen that the system has proposed for our
// window.
// Return Value:
// - <none>
void IslandWindow::SetCreateCallback(std::function<void(const HWND, const til::rect&)> pfn) noexcept
{
_pfnCreateCallback = pfn;
}
// Method Description:
// - Set a callback to be called when the window is being resized by user. For given
// requested window dimension (width or height, whichever border is dragged) it should
// return a resulting window dimension that is actually set. It is used to make the
// window 'snap' to the underling terminal's character grid.
// Arguments:
// - pfn: a function that transforms requested to actual window dimension.
// pfn's parameters:
// * widthOrHeight: whether the dimension is width (true) or height (false)
// * dimension: The requested dimension that comes from user dragging a border
// of the window. It is in pixels and represents only the client area.
// pfn's return value:
// * A dimension of client area that the window should resize to.
// Return Value:
// - <none>
void IslandWindow::SetSnapDimensionCallback(std::function<float(bool, float)> pfn) noexcept
{
_pfnSnapDimensionCallback = pfn;
}
// Method Description:
// - Handles a WM_CREATE message. Calls our create callback, if one's been set.
// Arguments:
// - wParam: unused
// - lParam: the lParam of a WM_CREATE, which is a pointer to a CREATESTRUCTW
// Return Value:
// - <none>
void IslandWindow::_HandleCreateWindow(const WPARAM, const LPARAM lParam) noexcept
{
// Get proposed window rect from create structure
auto pcs = reinterpret_cast<CREATESTRUCTW*>(lParam);
til::rect rc;
rc.left = pcs->x;
rc.top = pcs->y;
rc.right = rc.left + pcs->cx;
rc.bottom = rc.top + pcs->cy;
if (_pfnCreateCallback)
{
_pfnCreateCallback(_window.get(), rc);
}
// GH#11561: DO NOT call ShowWindow here. The AppHost will call ShowWindow
// once the app has completed its initialization.
UpdateWindow(_window.get());
UpdateWindowIconForActiveMetrics(_window.get());
}
// Method Description:
// - Handles a WM_SIZING message, which occurs when user drags a window border
// or corner. It intercepts this resize action and applies 'snapping' i.e.
// aligns the terminal's size to its cell grid. We're given the window size,
// which we then adjust based on the terminal's properties (like font size).
// Arguments:
// - wParam: Specifies which edge of the window is being dragged.
// - lParam: Pointer to the requested window rectangle (this is, the one that
// originates from current drag action). It also acts as the return value
// (it's a ref parameter).
// Return Value:
// - <none>
LRESULT IslandWindow::_OnSizing(const WPARAM wParam, const LPARAM lParam)
{
if (!_pfnSnapDimensionCallback)
{
// If we haven't been given the callback that would adjust the dimension,
// then we can't do anything, so just bail out.
return false;
}
auto winRect = reinterpret_cast<LPRECT>(lParam);
// If we're the quake window, prevent resizing on all sides except the
// bottom. This also applies to resizing with the Alt+Space menu
if (IsQuakeWindow() && wParam != WMSZ_BOTTOM)
{
// Stuff our current window size into the lParam, and return true. This
// will tell User32 to use our current dimensions to resize to.
::GetWindowRect(_window.get(), winRect);
return true;
}
// Find nearest monitor.
auto hmon = MonitorFromRect(winRect, MONITOR_DEFAULTTONEAREST);
// This API guarantees that dpix and dpiy will be equal, but neither is an
// optional parameter so give two UINTs.
UINT dpix = USER_DEFAULT_SCREEN_DPI;
UINT dpiy = USER_DEFAULT_SCREEN_DPI;
// If this fails, we'll use the default of 96. I think it can only fail for
// bad parameters, which we won't have, so no big deal.
LOG_IF_FAILED(GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy));
const auto nonClientSize = GetTotalNonClientExclusiveSize(dpix);
const auto dipPerPx = static_cast<float>(USER_DEFAULT_SCREEN_DPI) / static_cast<float>(dpix);
const auto pxPerDip = static_cast<float>(dpix) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
auto clientWidth = winRect->right - winRect->left - nonClientSize.width;
auto clientHeight = winRect->bottom - winRect->top - nonClientSize.height;
if (wParam != WMSZ_TOP && wParam != WMSZ_BOTTOM)
{
// If user has dragged anything but the top or bottom border (so e.g. left border,
// top-right corner etc.), then this means that the width has changed. We thus ask to
// adjust this new width so that terminal(s) is/are aligned to their character grid(s).
auto width = clientWidth * dipPerPx;
width = std::max(width, minimumWidth);
width = _pfnSnapDimensionCallback(true, width);
clientWidth = lroundf(width * pxPerDip);
}
if (wParam != WMSZ_LEFT && wParam != WMSZ_RIGHT)
{
// Analogous to above, but for height.
auto height = clientHeight * dipPerPx;
height = std::max(height, minimumHeight);
height = _pfnSnapDimensionCallback(false, height);
clientHeight = lroundf(height * pxPerDip);
}
// Now make the window rectangle match the calculated client width and height,
// regarding which border the user is dragging. E.g. if user drags left border, then
// we make sure to adjust the 'left' component of rectangle and not the 'right'. Note
// that top-left and bottom-left corners also 'include' left border, hence we match
// this in multi-case switch.
// Set width
switch (wParam)
{
case WMSZ_LEFT:
case WMSZ_TOPLEFT:
case WMSZ_BOTTOMLEFT:
winRect->left = winRect->right - (clientWidth + nonClientSize.width);
break;
case WMSZ_RIGHT:
case WMSZ_TOPRIGHT:
case WMSZ_BOTTOMRIGHT:
winRect->right = winRect->left + (clientWidth + nonClientSize.width);
break;
}
// Set height
switch (wParam)
{
case WMSZ_BOTTOM:
case WMSZ_BOTTOMLEFT:
case WMSZ_BOTTOMRIGHT:
winRect->bottom = winRect->top + (clientHeight + nonClientSize.height);
break;
case WMSZ_TOP:
case WMSZ_TOPLEFT:
case WMSZ_TOPRIGHT:
winRect->top = winRect->bottom - (clientHeight + nonClientSize.height);
break;
}
return true;
}
// Method Description:
// - Handle the WM_MOVING message
// - If we're the quake window, then we don't want to be able to be moved.
// Immediately return our current window position, which will prevent us from
// being moved at all.
// Arguments:
// - lParam: a LPRECT with the proposed window position, that should be filled
// with the resultant position.
// Return Value:
// - true iff we handled this message.
LRESULT IslandWindow::_OnMoving(const WPARAM /*wParam*/, const LPARAM lParam)
{
auto winRect = reinterpret_cast<LPRECT>(lParam);
// If we're the quake window, prevent moving the window. If we don't do
// this, then Alt+Space...Move will still be able to move the window.
if (IsQuakeWindow())
{
// Stuff our current window into the lParam, and return true. This
// will tell User32 to use our current position to move to.
::GetWindowRect(_window.get(), winRect);
return true;
}
return false;
}
// Method Description:
// - Start this window for the first time. This will instantiate our XAML
// island, set up our root grid, and initialize some other members that only
// need to be initialized once.
// - This should only be called once.
void IslandWindow::Initialize()
{
_source = DesktopWindowXamlSource{};
auto interop = _source.as<IDesktopWindowXamlSourceNative>();
winrt::check_hresult(interop->AttachToWindow(_window.get()));
// stash the child interop handle so we can resize it when the main hwnd is resized
interop->get_WindowHandle(&_interopWindowHandle);
// Immediately hide our XAML island hwnd. On earlier versions of Windows,
// this HWND could sometimes appear as an actual window in the taskbar
// without this!
ShowWindow(_interopWindowHandle, SW_HIDE);
_rootGrid = winrt::Windows::UI::Xaml::Controls::Grid();
_source.Content(_rootGrid);
// initialize the taskbar object
if (auto taskbar = wil::CoCreateInstanceNoThrow<ITaskbarList3>(CLSID_TaskbarList))
{
if (SUCCEEDED(taskbar->HrInit()))
{
_taskbar = std::move(taskbar);
}
}
_systemMenuNextItemId = IDM_SYSTEM_MENU_BEGIN;
// Enable vintage opacity by removing the XAML emergency backstop, GH#603.
// We don't really care if this failed or not.
TerminalTrySetTransparentBackground(true);
}
void IslandWindow::OnSize(const UINT width, const UINT height)
{
// NOTE: This _isn't_ called by NonClientIslandWindow::OnSize. The
// NonClientIslandWindow has very different logic for positioning the
// DesktopWindowXamlSource inside its HWND.
// update the interop window size
SetWindowPos(_interopWindowHandle, nullptr, 0, 0, width, height, SWP_SHOWWINDOW | SWP_NOACTIVATE);
if (_rootGrid)
{
const auto size = GetLogicalSize();
_rootGrid.Width(size.Width);
_rootGrid.Height(size.Height);
}
}
// Method Description:
// - Handles a WM_GETMINMAXINFO message, issued before the window sizing starts.
// This message allows to modify the minimal and maximal dimensions of the window.
// We focus on minimal dimensions here
// (the maximal dimension will be calculate upon maximizing)
// Our goal is to protect against to downsizing to less than minimal allowed dimensions,
// that might occur in the scenarios where _OnSizing is bypassed.
// An example of such scenario is anchoring the window to the top/bottom screen border
// in order to maximize window height (GH# 8026).
// The computation is similar to what we do in _OnSizing:
// we need to consider both the client area and non-client exclusive area sizes,
// while taking DPI into account as well.
// Arguments:
// - lParam: Pointer to the requested MINMAXINFO struct,
// a ptMinTrackSize field of which we want to update with the computed dimensions.
// It also acts as the return value (it's a ref parameter).
// Return Value:
// - <none>
void IslandWindow::_OnGetMinMaxInfo(const WPARAM /*wParam*/, const LPARAM lParam)
{
// Without a callback we don't know to snap the dimensions of the client area.
// Should not be a problem, the callback is not set early in the startup
// The initial dimensions will be set later on
if (!_pfnSnapDimensionCallback)
{
return;
}
auto hmon = MonitorFromWindow(GetHandle(), MONITOR_DEFAULTTONEAREST);
if (hmon == NULL)
{
return;
}
UINT dpix = USER_DEFAULT_SCREEN_DPI;
UINT dpiy = USER_DEFAULT_SCREEN_DPI;
GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy);
// From now we use dpix for all computations (same as in _OnSizing).
const auto nonClientSizeScaled = GetTotalNonClientExclusiveSize(dpix);
const auto pxPerDip = static_cast<float>(dpix) / static_cast<float>(USER_DEFAULT_SCREEN_DPI);
// We might have been called in WM_CREATE, before we've initialized XAML or
// our page. That's okay.
const auto width = _pfnSnapDimensionCallback(true, minimumWidth);
const auto height = _pfnSnapDimensionCallback(false, minimumHeight);
auto lpMinMaxInfo = reinterpret_cast<LPMINMAXINFO>(lParam);
lpMinMaxInfo->ptMinTrackSize.x = lroundf(width * pxPerDip) + nonClientSizeScaled.width;
lpMinMaxInfo->ptMinTrackSize.y = lroundf(height * pxPerDip) + nonClientSizeScaled.height;
}
[[nodiscard]] LRESULT IslandWindow::MessageHandler(UINT const message, WPARAM const wparam, LPARAM const lparam) noexcept
{
if (IsCursorHidden())
{
ShowCursorMaybe(message);
}
switch (message)
{
case WM_GETMINMAXINFO:
{
_OnGetMinMaxInfo(wparam, lparam);
return 0;
}
case WM_CREATE:
{
_HandleCreateWindow(wparam, lparam);
return 0;
}
case WM_ENABLE:
{
if (_interopWindowHandle != nullptr)
{
// send focus to the child window
SetFocus(_interopWindowHandle);
}
break;
}
case WM_SETFOCUS:
{
if (_interopWindowHandle != nullptr)
{
// send focus to the child window
SetFocus(_interopWindowHandle);
return 0;
}
break;
}
case WM_ACTIVATE:
{
// wparam = 0 indicates the window was deactivated
const bool activated = LOWORD(wparam) != 0;
WindowActivated.raise(activated);
if (_autoHideWindow && !activated)
{
if (_isQuakeWindow || _minimizeToNotificationArea)
{
HideWindow();
}
else
{
ShowWindow(GetHandle(), SW_MINIMIZE);
}
}
break;
}
case WM_NCLBUTTONDOWN:
case WM_NCLBUTTONUP:
case WM_NCMBUTTONDOWN:
case WM_NCMBUTTONUP:
case WM_NCRBUTTONDOWN:
case WM_NCRBUTTONUP:
case WM_NCXBUTTONDOWN:
case WM_NCXBUTTONUP:
{
// If we clicked in the titlebar, raise an event so the app host can
// dispatch an appropriate event.
DragRegionClicked.raise();
break;
}
case WM_MENUCHAR:
{
// GH#891: return this LRESULT here to prevent the app from making a
// bell when alt+key is pressed. A menu is active and the user presses a
// key that does not correspond to any mnemonic or accelerator key,
return MAKELRESULT(0, MNC_CLOSE);
}
case WM_SIZING:
{
return _OnSizing(wparam, lparam);
}
case WM_SIZE:
{
if (wparam == SIZE_RESTORED || wparam == SIZE_MAXIMIZED)
{
WindowVisibilityChanged.raise(true);
MaximizeChanged.raise(wparam == SIZE_MAXIMIZED);
}
if (wparam == SIZE_MINIMIZED)
{
WindowVisibilityChanged.raise(false);
if (_isQuakeWindow)
{
ShowWindow(GetHandle(), SW_HIDE);
return 0;
}
}
break;
}
case WM_MOVING:
{
return _OnMoving(wparam, lparam);
}
case WM_MOVE:
{
WindowMoved.raise();
break;
}
case WM_CLOSE:
{
// If the user wants to close the app by clicking 'X' button,
// we hand off the close experience to the app layer. If all the tabs
// are closed, the window will be closed as well.
WindowCloseButtonClicked.raise();
return 0;
}
case WM_MOUSEWHEEL:
try
{
// This whole handler is a hack for GH#979.
//
// On some laptops, their trackpads won't scroll inactive windows
// _ever_. With our entire window just being one giant XAML Island, the
// touchpad driver thinks our entire window is inactive, and won't
// scroll the XAML island. On those types of laptops, we'll get a
// WM_MOUSEWHEEL here, in our root window, when the trackpad scrolls.
// We're going to take that message and manually plumb it through to our
// TermControl's, or anything else that implements IMouseWheelListener.
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms645617(v=vs.85).aspx
// Important! Do not use the LOWORD or HIWORD macros to extract the x-
// and y- coordinates of the cursor position because these macros return
// incorrect results on systems with multiple monitors. Systems with
// multiple monitors can have negative x- and y- coordinates, and LOWORD
// and HIWORD treat the coordinates as unsigned quantities.
const til::point eventPoint{ GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam) };
// This mouse event is relative to the display origin, not the window. Convert here.
const til::rect windowRect{ GetWindowRect() };
const auto origin = windowRect.origin();
const auto relative = eventPoint - origin;
// Convert to logical scaling before raising the event.
const auto scale = GetCurrentDpiScale();
const winrt::Windows::Foundation::Point real{ relative.x / scale, relative.y / scale };
const auto wheelDelta = static_cast<short>(HIWORD(wparam));
// Raise an event, so any listeners can handle the mouse wheel event manually.
MouseScrolled.raise(real, wheelDelta);
return 0;
}
CATCH_LOG();
break;
case WM_THEMECHANGED:
UpdateWindowIconForActiveMetrics(_window.get());
return 0;
case WM_WINDOWPOSCHANGING:
{
// GH#10274 - if the quake window gets moved to another monitor via aero
// snap (win+shift+arrows), then re-adjust the size for the new monitor.
if (IsQuakeWindow())
{
// Retrieve the suggested dimensions and make a rect and size.
auto lpwpos = (LPWINDOWPOS)lparam;
// We only need to apply restrictions if the position is changing.
// The SWP_ flags are confusing to read. This is
// "if we're not moving the window, do nothing."
if (WI_IsFlagSet(lpwpos->flags, SWP_NOMOVE))
{
break;
}
// Figure out the suggested dimensions and position.
RECT rcSuggested;
rcSuggested.left = lpwpos->x;
rcSuggested.top = lpwpos->y;
rcSuggested.right = rcSuggested.left + lpwpos->cx;
rcSuggested.bottom = rcSuggested.top + lpwpos->cy;
// Find the bounds of the current monitor, and the monitor that
// we're suggested to be on.
auto current = MonitorFromWindow(_window.get(), MONITOR_DEFAULTTONEAREST);
MONITORINFO currentInfo;
currentInfo.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(current, &currentInfo);
auto proposed = MonitorFromRect(&rcSuggested, MONITOR_DEFAULTTONEAREST);
MONITORINFO proposedInfo;
proposedInfo.cbSize = sizeof(MONITORINFO);
GetMonitorInfo(proposed, &proposedInfo);
// If the monitor changed...
if (til::rect{ proposedInfo.rcMonitor } !=
til::rect{ currentInfo.rcMonitor })
{
const auto newWindowRect{ _getQuakeModeSize(proposed) };
// Inform User32 that we want to be placed at the position
// and dimensions that _getQuakeModeSize returned. When we
// snap across monitor boundaries, this will re-evaluate our
// size for the new monitor.
lpwpos->x = newWindowRect.left;
lpwpos->y = newWindowRect.top;
lpwpos->cx = newWindowRect.width();
lpwpos->cy = newWindowRect.height();
return 0;
}
}
break;
}
case WM_SYSCOMMAND:
{
// the low 4 bits contain additional information (that we don't care about)
auto highBits = wparam & 0xFFF0;
if (highBits == SC_RESTORE || highBits == SC_MAXIMIZE)
{
MaximizeChanged.raise(highBits == SC_MAXIMIZE);
}
if (wparam == SC_RESTORE && _fullscreen)
{
ShouldExitFullscreen.raise();
return 0;
}
auto search = _systemMenuItems.find(LOWORD(wparam));
if (search != _systemMenuItems.end())
{
search->second.callback();
}
break;
}
}
// TODO: handle messages here...
return base_type::MessageHandler(message, wparam, lparam);
}
// Method Description:
// - Called when the window has been resized (or maximized)
// Arguments:
// - width: the new width of the window _in pixels_
// - height: the new height of the window _in pixels_
void IslandWindow::OnResize(const UINT width, const UINT height)
{
if (_interopWindowHandle)
{
OnSize(width, height);
}
}
// Method Description:
// - Called when the window is minimized to the taskbar.
void IslandWindow::OnMinimize()
{
// TODO GH#1989 Stop rendering island content when the app is minimized.
if (_minimizeToNotificationArea)
{
HideWindow();
}
}
// Method Description:
// - Called when the window is restored from having been minimized.
void IslandWindow::OnRestore()
{
// TODO GH#1989 Stop rendering island content when the app is minimized.
}
void IslandWindow::SetContent(winrt::Windows::UI::Xaml::UIElement content)
{
_rootGrid.Children().Clear();
_rootGrid.Children().Append(content);
}
// Method Description:
// - Get the dimensions of our non-client area, as a rect where each component
// represents that side.
// - The .left will be a negative number, to represent that the actual side of
// the non-client area is outside the border of our window. It's roughly 8px (
// * DPI scaling) to the left of the visible border.
// - The .right component will be positive, indicating that the nonclient border
// is in the positive-x direction from the edge of our client area.
// - This will also include our titlebar! It's in the nonclient area for us.
// Arguments:
// - dpi: the scaling that we should use to calculate the border sizes.
// Return Value:
// - a til::rect whose components represent the margins of the nonclient area,
// relative to the client area.
til::rect IslandWindow::GetNonClientFrame(const UINT dpi) const noexcept
{
const auto windowStyle = static_cast<DWORD>(GetWindowLong(_window.get(), GWL_STYLE));
RECT islandFrame{};
// If we failed to get the correct window size for whatever reason, log
// the error and go on. We'll use whatever the control proposed as the
// size of our window, which will be at least close.
LOG_IF_WIN32_BOOL_FALSE(AdjustWindowRectExForDpi(&islandFrame, windowStyle, false, 0, dpi));
return til::rect{ islandFrame };
}
// Method Description:
// - Gets the difference between window and client area size.
// Arguments:
// - dpi: dpi of a monitor on which the window is placed
// Return Value
// - The size difference
til::size IslandWindow::GetTotalNonClientExclusiveSize(const UINT dpi) const noexcept
{
const auto islandFrame{ GetNonClientFrame(dpi) };
return {
islandFrame.right - islandFrame.left,
islandFrame.bottom - islandFrame.top
};
}
void IslandWindow::OnAppInitialized()
{
// Do a quick resize to force the island to paint
const auto size = GetPhysicalSize();
OnSize(size.width, size.height);
}
// Method Description:
// - Called when the app wants to change its theme. We'll update the root UI
// element of the entire XAML tree, so that all UI elements get the theme
// applied.
// Arguments:
// - arg: the ElementTheme to use as the new theme for the UI
// Return Value:
// - <none>
void IslandWindow::OnApplicationThemeChanged(const winrt::Windows::UI::Xaml::ElementTheme& requestedTheme)
{
_rootGrid.RequestedTheme(requestedTheme);
// Invalidate the window rect, so that we'll repaint any elements we're
// drawing ourselves to match the new theme
::InvalidateRect(_window.get(), nullptr, false);
}
// Method Description:
// - Updates our focus mode state. See _SetIsBorderless for more details.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::FocusModeChanged(const bool focusMode)
{
// Do nothing if the value was unchanged.
if (focusMode == _borderless)
{
return;
}
_SetIsBorderless(focusMode);
}
// Method Description:
// - Updates our fullscreen state. See _SetIsFullscreen for more details.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::FullscreenChanged(const bool fullscreen)
{
// Do nothing if the value was unchanged.
if (fullscreen == _fullscreen)
{
return;
}
_SetIsFullscreen(fullscreen);
}
// Method Description:
// - Enter or exit the "always on top" state. Before the window is created, this
// value will later be used when we create the window to create the window on
// top of all others. After the window is created, it will either enter the
// group of topmost windows, or exit the group of topmost windows.
// Arguments:
// - alwaysOnTop: whether we should be entering or exiting always on top mode.
// Return Value:
// - <none>
void IslandWindow::SetAlwaysOnTop(const bool alwaysOnTop)
{
_alwaysOnTop = alwaysOnTop;
const auto hwnd = GetHandle();
if (hwnd)
{
SetWindowPos(hwnd,
_alwaysOnTop ? HWND_TOPMOST : HWND_NOTOPMOST,
0, // the window dimensions are unused, because we're passing SWP_NOSIZE
0,
0,
0,
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
}
// Method Description:
// - Posts a message to the window message queue that the window visibility has changed
// and should then be minimized or restored.
// Arguments:
// - showOrHide: True for show; false for hide.
// Return Value:
// - <none>
void IslandWindow::ShowWindowChanged(const bool showOrHide)
{
if (const auto hwnd = GetHandle())
{
// IMPORTANT!
//
// ONLY "restore" if already minimized. If the window is maximized or
// snapped, a restore will restore-down the window instead.
if (showOrHide == true && ::IsIconic(hwnd))
{
::PostMessage(hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);
}
else if (showOrHide == false)
{
::PostMessage(hwnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);
}
}
}
void IslandWindow::SetShowTabsFullscreen(const bool newShowTabsFullscreen)
{
_showTabsFullscreen = newShowTabsFullscreen;
}
// Method Description
// - Flash the taskbar icon, indicating to the user that something needs their attention
void IslandWindow::FlashTaskbar()
{
// Using 'false' as the boolean argument ensures that the taskbar is
// only flashed if the app window is not focused
FlashWindow(_window.get(), false);
}
// Method Description:
// - Sets the taskbar progress indicator
// - We follow the ConEmu style for the state and progress values,
// more details at https://conemu.github.io/en/AnsiEscapeCodes.html#ConEmu_specific_OSC
// Arguments:
// - state: indicates the progress state
// - progress: indicates the progress value
void IslandWindow::SetTaskbarProgress(const size_t state, const size_t progress)
{
if (_taskbar)
{
switch (state)
{
case 0:
// removes the taskbar progress indicator
_taskbar->SetProgressState(_window.get(), TBPF_NOPROGRESS);
break;
case 1:
// sets the progress value to value given by the 'progress' parameter
_taskbar->SetProgressState(_window.get(), TBPF_NORMAL);
_taskbar->SetProgressValue(_window.get(), progress, 100);
break;
case 2:
// sets the progress indicator to an error state
_taskbar->SetProgressState(_window.get(), TBPF_ERROR);
_taskbar->SetProgressValue(_window.get(), progress, 100);
break;
case 3:
// sets the progress indicator to an indeterminate state.
// FIRST, set the progress to "no progress". That'll clear out any
// progress value from the previous state. Otherwise, a transition
// from (error,x%) or (warning,x%) to indeterminate will leave the
// progress value unchanged, and not show the spinner.
_taskbar->SetProgressState(_window.get(), TBPF_NOPROGRESS);
_taskbar->SetProgressState(_window.get(), TBPF_INDETERMINATE);
break;
case 4:
// sets the progress indicator to a pause state
_taskbar->SetProgressState(_window.get(), TBPF_PAUSED);
_taskbar->SetProgressValue(_window.get(), progress, 100);
break;
default:
break;
}
}
}
// From GdiEngine::s_SetWindowLongWHelper
static void SetWindowLongWHelper(const HWND hWnd, const int nIndex, const LONG dwNewLong) noexcept
{
// SetWindowLong has strange error handling. On success, it returns the
// previous Window Long value and doesn't modify the Last Error state. To
// deal with this, we set the last error to 0/S_OK first, call it, and if
// the previous long was 0, we check if the error was non-zero before
// reporting. Otherwise, we'll get an "Error: The operation has completed
// successfully." and there will be another screenshot on the internet
// making fun of Windows. See:
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633591(v=vs.85).aspx
SetLastError(0);
const auto lResult = SetWindowLongW(hWnd, nIndex, dwNewLong);
if (0 == lResult)
{
LOG_LAST_ERROR_IF(0 != GetLastError());
}
}
// Method Description:
// - This is a helper to figure out what the window styles should be, given the
// current state of flags like borderless mode and fullscreen mode.
// Arguments:
// - <none>
// Return Value:
// - a LONG with the appropriate flags set for our current window mode, to be used with GWL_STYLE
LONG IslandWindow::_getDesiredWindowStyle() const
{
auto windowStyle = GetWindowLongW(GetHandle(), GWL_STYLE);
// If we're both fullscreen and borderless, fullscreen mode takes precedence.
if (_fullscreen)
{
// When moving to fullscreen, remove WS_OVERLAPPEDWINDOW, which specifies
// styles for non-fullscreen windows (e.g. caption bar), and add the
// WS_POPUP style to allow us to size ourselves to the monitor size.
// Do the reverse when restoring from fullscreen.
// Doing these modifications to that window will cause a vista-style
// window frame to briefly appear when entering and exiting fullscreen.
WI_ClearFlag(windowStyle, WS_BORDER);
WI_ClearFlag(windowStyle, WS_SIZEBOX);
WI_ClearAllFlags(windowStyle, WS_OVERLAPPEDWINDOW);
WI_SetFlag(windowStyle, WS_POPUP);
return windowStyle;
}
else if (_borderless)
{
// When moving to borderless, remove WS_OVERLAPPEDWINDOW, which
// specifies styles for non-fullscreen windows (e.g. caption bar), and
// add the WS_BORDER and WS_SIZEBOX styles. This allows us to still have
// a small resizing frame, but without a full titlebar, nor caption
// buttons.
WI_ClearAllFlags(windowStyle, WS_OVERLAPPEDWINDOW);
WI_ClearFlag(windowStyle, WS_POPUP);
WI_SetFlag(windowStyle, WS_BORDER);
WI_SetFlag(windowStyle, WS_SIZEBOX);
return windowStyle;
}
// Here, we're not in either fullscreen or borderless mode. Return to
// WS_OVERLAPPEDWINDOW.
WI_ClearFlag(windowStyle, WS_POPUP);
WI_ClearFlag(windowStyle, WS_BORDER);
WI_ClearFlag(windowStyle, WS_SIZEBOX);
WI_SetAllFlags(windowStyle, WS_OVERLAPPEDWINDOW);
return windowStyle;
}
// Method Description:
// - Enable or disable focus mode. When entering focus mode, we'll
// need to manually hide the entire titlebar.
// - When we're entering focus we need to do some additional modification
// of our window styles. However, the NonClientIslandWindow very explicitly
// _doesn't_ need to do these steps.
// Arguments:
// - borderlessEnabled: If true, we're entering focus mode. If false, we're leaving.
// Return Value:
// - <none>
void IslandWindow::_SetIsBorderless(const bool borderlessEnabled)
{
_borderless = borderlessEnabled;
const auto hWnd = GetHandle();
// First, modify regular window styles as appropriate
auto windowStyle = _getDesiredWindowStyle();
SetWindowLongWHelper(hWnd, GWL_STYLE, windowStyle);
// Now modify extended window styles as appropriate
// When moving to fullscreen, remove the window edge style to avoid an
// ugly border when not focused.
auto exWindowStyle = GetWindowLongW(hWnd, GWL_EXSTYLE);
WI_UpdateFlag(exWindowStyle, WS_EX_WINDOWEDGE, !_fullscreen);
SetWindowLongWHelper(hWnd, GWL_EXSTYLE, exWindowStyle);
// Resize the window, with SWP_FRAMECHANGED, to trigger user32 to
// recalculate the non/client areas
const til::rect windowPos{ GetWindowRect() };
SetWindowPos(GetHandle(),
HWND_TOP,
windowPos.left,
windowPos.top,
windowPos.width(),
windowPos.height(),
SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOACTIVATE);
}
// Method Description:
// - Called when entering fullscreen, with the window's current monitor rect and work area.
// - The current window position, dpi, work area, and maximized state are stored, and the
// window is positioned to the monitor rect.
void IslandWindow::_SetFullscreenPosition(const RECT& rcMonitor, const RECT& rcWork)
{
const auto hWnd = GetHandle();
::GetWindowRect(hWnd, &_rcWindowBeforeFullscreen);
_dpiBeforeFullscreen = GetDpiForWindow(hWnd);
_fWasMaximizedBeforeFullscreen = IsZoomed(hWnd);
_rcWorkBeforeFullscreen = rcWork;
SetWindowPos(hWnd,
HWND_TOP,
rcMonitor.left,
rcMonitor.top,
rcMonitor.right - rcMonitor.left,
rcMonitor.bottom - rcMonitor.top,
SWP_FRAMECHANGED);
}
// Method Description:
// - Called when exiting fullscreen, with the window's current monitor work area.
// - The window is restored to its previous position, migrating that previous position to the
// window's current monitor (if the current work area or window DPI have changed).
// - A fullscreen window's monitor can be changed by win+shift+left/right hotkeys or monitor
// topology changes (for example unplugging a monitor or disconnecting a remote session).
void IslandWindow::_RestoreFullscreenPosition(const RECT& rcWork)
{
const auto hWnd = GetHandle();
// If the window was previously maximized, re-maximize the window.
if (_fWasMaximizedBeforeFullscreen)
{
ShowWindow(hWnd, SW_SHOWMAXIMIZED);
SetWindowPos(hWnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
return;
}
// Start with the stored window position.
auto rcRestore = _rcWindowBeforeFullscreen;
// If the window DPI has changed, re-size the stored position by the change in DPI. This
// ensures the window restores to the same logical size (even if to a monitor with a different
// DPI/ scale factor).
auto dpiWindow = GetDpiForWindow(hWnd);
rcRestore.right = rcRestore.left + MulDiv(rcRestore.right - rcRestore.left, dpiWindow, _dpiBeforeFullscreen);
rcRestore.bottom = rcRestore.top + MulDiv(rcRestore.bottom - rcRestore.top, dpiWindow, _dpiBeforeFullscreen);
// Offset the stored position by the difference in work area.
OffsetRect(&rcRestore,
rcWork.left - _rcWorkBeforeFullscreen.left,
rcWork.top - _rcWorkBeforeFullscreen.top);
const til::size ncSize{ GetTotalNonClientExclusiveSize(dpiWindow) };
auto rcWorkAdjusted = rcWork;
// GH#10199 - adjust the size of the "work" rect by the size of our borders.
// We want to make sure the window is restored within the bounds of the
// monitor we're on, but it's totally fine if the invisible borders are
// outside the monitor.
const auto halfWidth{ ncSize.width / 2 };
const auto halfHeight{ ncSize.height / 2 };
rcWorkAdjusted.left -= halfWidth;
rcWorkAdjusted.right += halfWidth;
rcWorkAdjusted.top -= halfHeight;
rcWorkAdjusted.bottom += halfHeight;
// Enforce that our position is entirely within the bounds of our work area.
// Prefer the top-left be on-screen rather than bottom-right (right before left, bottom before top).
if (rcRestore.right > rcWorkAdjusted.right)
{
OffsetRect(&rcRestore, rcWorkAdjusted.right - rcRestore.right, 0);
}
if (rcRestore.left < rcWorkAdjusted.left)
{
OffsetRect(&rcRestore, rcWorkAdjusted.left - rcRestore.left, 0);
}
if (rcRestore.bottom > rcWorkAdjusted.bottom)
{
OffsetRect(&rcRestore, 0, rcWorkAdjusted.bottom - rcRestore.bottom);
}
if (rcRestore.top < rcWorkAdjusted.top)
{
OffsetRect(&rcRestore, 0, rcWorkAdjusted.top - rcRestore.top);
}
// Show the window at the computed position.
SetWindowPos(hWnd,
HWND_TOP,
rcRestore.left,
rcRestore.top,
rcRestore.right - rcRestore.left,
rcRestore.bottom - rcRestore.top,
SWP_SHOWWINDOW | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED);
}
// Method Description:
// - Controls setting us into or out of fullscreen mode. Largely taken from
// Window::SetIsFullscreen in conhost.
// - When entering fullscreen mode, we'll save the current window size and
// location, and expand to take the entire monitor size. When leaving, we'll
// use that saved size to restore back to.
// Arguments:
// - fullscreenEnabled true if we should enable fullscreen mode, false to disable.
// Return Value:
// - <none>
void IslandWindow::_SetIsFullscreen(const bool fullscreenEnabled)
{
// It is possible to enter _SetIsFullscreen even if we're already in full
// screen. Use the old is in fullscreen flag to gate checks that rely on the
// current state.
const auto fChangingFullscreen = (fullscreenEnabled != _fullscreen);
_fullscreen = fullscreenEnabled;
const auto hWnd = GetHandle();
// First, modify regular window styles as appropriate
auto windowStyle = _getDesiredWindowStyle();
SetWindowLongWHelper(hWnd, GWL_STYLE, windowStyle);
// Now modify extended window styles as appropriate
// When moving to fullscreen, remove the window edge style to avoid an
// ugly border when not focused.
auto exWindowStyle = GetWindowLongW(hWnd, GWL_EXSTYLE);
WI_UpdateFlag(exWindowStyle, WS_EX_WINDOWEDGE, !_fullscreen);
SetWindowLongWHelper(hWnd, GWL_EXSTYLE, exWindowStyle);
// Only change the window position if changing fullscreen state.
if (fChangingFullscreen)
{
// Get the monitor info for the window's current monitor.
MONITORINFO mi = {};
mi.cbSize = sizeof(mi);
GetMonitorInfo(MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST), &mi);
if (_fullscreen)
{
// Store the window's current position and size the window to the monitor.
_SetFullscreenPosition(mi.rcMonitor, mi.rcWork);
}
else
{
// Restore the stored window position.
_RestoreFullscreenPosition(mi.rcWork);
}
}
}
// Method Description:
// - Summon the window, or possibly dismiss it. If toggleVisibility is true,
// then we'll dismiss (minimize) the window if it's currently active.
// Otherwise, we'll always just try to activate the window.
// Arguments:
// - toggleVisibility: controls how we should behave when already in the foreground.
// Return Value:
// - <none>
void IslandWindow::SummonWindow(winrt::TerminalApp::SummonWindowBehavior args)
{
const auto toggleVisibility = args ? args.ToggleVisibility() : false;
const auto toMonitor = args ? args.ToMonitor() : winrt::TerminalApp::MonitorBehavior::InPlace;
auto dropdownDuration = args ? args.DropdownDuration() : 0;
// If the user requested an animation, let's check if animations are enabled in the OS.
if (dropdownDuration > 0)
{
auto animationsEnabled = TRUE;
SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0);
if (!animationsEnabled)
{
// The OS has animations disabled - we should respect that and
// disable the animation here.
//
// We're doing this here, rather than in _doSlideAnimation, to
// preempt any other specific behavior that
// _globalActivateWindow/_globalDismissWindow might do if they think
// there should be an animation (like making the window appear with
// SetWindowPlacement rather than ShowWindow)
dropdownDuration = 0;
}
}
// * If the user doesn't want to toggleVisibility, then just always try to
// activate.
// * If the user does want to toggleVisibility,
// - If we're the foreground window, ToMonitor == ToMouse, and the mouse is on the monitor we are
// - activate the window
// - else
// - dismiss the window
if (toggleVisibility && GetForegroundWindow() == _window.get())
{
auto handled = false;
// They want to toggle the window when it is the FG window, and we are
// the FG window. However, if we're on a different monitor than the
// mouse, then we should move to that monitor instead of dismissing.
if (toMonitor == winrt::TerminalApp::MonitorBehavior::ToMouse)
{
const til::rect cursorMonitorRect{ _getMonitorForCursor().rcMonitor };
const til::rect currentMonitorRect{ _getMonitorForWindow(GetHandle()).rcMonitor };
if (cursorMonitorRect != currentMonitorRect)
{
// We're not on the same monitor as the mouse. Go to that monitor.
_globalActivateWindow(dropdownDuration, toMonitor);
handled = true;
}
}
if (!handled)
{
_globalDismissWindow(dropdownDuration);
}
}
else
{
_globalActivateWindow(dropdownDuration, toMonitor);
}
}
// Method Description:
// - Helper for performing a sliding animation. This will animate our _Xaml
// Island_, either growing down or shrinking up, using SetWindowRgn.
// - This function does the entire animation on the main thread (the UI thread),
// and **DOES NOT YIELD IT**. The window will be animating for the entire
// duration of dropdownDuration.
// - At the end of the animation, we'll reset the window region, so that it's as
// if nothing occurred.
// Arguments:
// - dropdownDuration: The duration to play the animation, in milliseconds. If
// 0, we won't perform a dropdown animation.
// - down: if true, increase the height from top to bottom. otherwise, decrease
// the height, from bottom to top.
// Return Value:
// - <none>
void IslandWindow::_doSlideAnimation(const uint32_t dropdownDuration, const bool down)
{
til::rect fullWindowSize{ GetWindowRect() };
const auto fullHeight = fullWindowSize.height();
const double animationDuration = dropdownDuration; // use floating-point math throughout
const auto start = std::chrono::system_clock::now();
// Do at most dropdownDuration frames. After that, just bail straight to the
// final state.
for (uint32_t i = 0; i < dropdownDuration; i++)
{
const auto end = std::chrono::system_clock::now();
const auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
const auto dt = static_cast<double>(millis.count());
if (dt > animationDuration)
{
break;
}
// If going down, increase the height over time. If going up, decrease the height.
const auto currentHeight = ::base::saturated_cast<int>(
down ? ((dt / animationDuration) * fullHeight) :
((1.0 - (dt / animationDuration)) * fullHeight));
wil::unique_hrgn rgn{ CreateRectRgn(0, 0, fullWindowSize.width(), currentHeight) };
SetWindowRgn(_interopWindowHandle, rgn.get(), true);
// Go immediately into another frame. This prevents the window from
// doing anything else (tearing our state). A Sleep() here will cause a
// weird stutter, and causes the animation to not be as smooth.
}
// Reset the window.
SetWindowRgn(_interopWindowHandle, nullptr, true);
}
void IslandWindow::_dropdownWindow(const uint32_t dropdownDuration,
const winrt::TerminalApp::MonitorBehavior toMonitor)
{
// First, get the window that's currently in the foreground. We'll need
// _this_ window to be able to appear on top of. If we just use
// GetForegroundWindow after the SetWindowPlacement call, _we_ will be the
// foreground window.
const auto oldForegroundWindow = GetForegroundWindow();
// First, restore the window. SetWindowPlacement has a fun undocumented
// piece of functionality where it will restore the window position
// _without_ the animation, so use that instead of ShowWindow(SW_RESTORE).
WINDOWPLACEMENT wpc{};
wpc.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(_window.get(), &wpc);
// If the window is hidden, SW_SHOW it first.
if (!IsWindowVisible(GetHandle()))
{
wpc.showCmd = SW_SHOW;
SetWindowPlacement(_window.get(), &wpc);
}
wpc.showCmd = SW_RESTORE;
SetWindowPlacement(_window.get(), &wpc);
// Possibly go to the monitor of the mouse / old foreground window.
_moveToMonitor(oldForegroundWindow, toMonitor);
// Now that we're visible, animate the dropdown.
_doSlideAnimation(dropdownDuration, true);
}
void IslandWindow::_slideUpWindow(const uint32_t dropdownDuration)
{
// First, animate the window sliding up.
_doSlideAnimation(dropdownDuration, false);
// Then, use SetWindowPlacement to minimize without the animation.
WINDOWPLACEMENT wpc{};
wpc.length = sizeof(WINDOWPLACEMENT);
GetWindowPlacement(_window.get(), &wpc);
wpc.showCmd = SW_MINIMIZE;
SetWindowPlacement(_window.get(), &wpc);
}
// Method Description:
// - Force activate this window. This method will bring us to the foreground and
// activate us. If the window is minimized, it will restore the window. If the
// window is on another desktop, the OS will switch to that desktop.
// - If the window is minimized, and dropdownDuration is greater than 0, we'll
// perform a "slide in" animation. We won't do this if the window is already
// on the screen (since that seems silly).
// Arguments:
// - dropdownDuration: The duration to play the dropdown animation, in
// milliseconds. If 0, we won't perform a dropdown animation.
// Return Value:
// - <none>
void IslandWindow::_globalActivateWindow(const uint32_t dropdownDuration,
const winrt::TerminalApp::MonitorBehavior toMonitor)
{
// First, get the window that's currently in the foreground. We'll need
// _this_ window to be able to appear on top of. If we just use
// GetForegroundWindow after the SetWindowPlacement/ShowWindow call, _we_
// will be the foreground window.
const auto oldForegroundWindow = GetForegroundWindow();
// From: https://stackoverflow.com/a/59659421
// > The trick is to make windows think that our process and the target
// > window (hwnd) are related by attaching the threads (using
// > AttachThreadInput API) and using an alternative API: BringWindowToTop.
// If the window is minimized, then restore it. We don't want to do this
// always though, because if you SW_RESTORE a maximized window, it will
// restore-down the window.
if (IsIconic(_window.get()))
{
if (dropdownDuration > 0)
{
_dropdownWindow(dropdownDuration, toMonitor);
}
else
{
// If the window is hidden, SW_SHOW it first. Note that hidden !=
// minimized. A hidden window doesn't appear in the taskbar, while a
// minimized window will. If you don't do this, then we won't be
// able to properly set this as the foreground window.
if (!IsWindowVisible(GetHandle()))
{
ShowWindow(_window.get(), SW_SHOW);
}
ShowWindow(_window.get(), SW_RESTORE);
// Once we've been restored, throw us on the active monitor.
_moveToMonitor(oldForegroundWindow, toMonitor);
}
}
else
{
// Try first to send a message to the current foreground window. If it's not responding, it may
// be waiting on us to finish launching. Passing SMTO_NOTIMEOUTIFNOTHUNG means that we get the same
// behavior as before--that is, waiting for the message loop--but we've done an early return if
// it turns out that it was hung.
// SendMessageTimeoutW returns nonzero if it succeeds.
if (0 != SendMessageTimeoutW(oldForegroundWindow, WM_NULL, 0, 0, SMTO_NOTIMEOUTIFNOTHUNG | SMTO_BLOCK | SMTO_ABORTIFHUNG, 1000, nullptr))
{
const auto windowThreadProcessId = GetWindowThreadProcessId(oldForegroundWindow, nullptr);
const auto currentThreadId = GetCurrentThreadId();
LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, true));
// Just in case, add the thread detach as a scope_exit, to make _sure_ we do it.
auto detachThread = wil::scope_exit([windowThreadProcessId, currentThreadId]() {
LOG_IF_WIN32_BOOL_FALSE(AttachThreadInput(windowThreadProcessId, currentThreadId, false));
});
LOG_IF_WIN32_BOOL_FALSE(BringWindowToTop(_window.get()));
ShowWindow(_window.get(), SW_SHOW);
// Activate the window too. This will force us to the virtual desktop this
// window is on, if it's on another virtual desktop.
LOG_LAST_ERROR_IF_NULL(SetActiveWindow(_window.get()));
// Throw us on the active monitor.
_moveToMonitor(oldForegroundWindow, toMonitor);
}
}
}
// Method Description:
// - Minimize the window. This is called when the window is summoned, but is
// already active
// - If dropdownDuration is greater than 0, we'll perform a "slide in"
// animation, before minimizing the window.
// Arguments:
// - dropdownDuration: The duration to play the slide-up animation, in
// milliseconds. If 0, we won't perform a slide-up animation.
// Return Value:
// - <none>
void IslandWindow::_globalDismissWindow(const uint32_t dropdownDuration)
{
if (dropdownDuration > 0)
{
_slideUpWindow(dropdownDuration);
}
else
{
ShowWindow(_window.get(), SW_MINIMIZE);
}
}
// Method Description:
// - Get the monitor the mouse cursor is currently on
// Arguments:
// - dropdownDuration: The duration to play the slide-up animation, in
// milliseconds. If 0, we won't perform a slide-up animation.
// Return Value:
// - The MONITORINFO for the monitor the mouse cursor is on
MONITORINFO IslandWindow::_getMonitorForCursor()
{
POINT p{};
GetCursorPos(&p);
// Get the monitor info for the window's current monitor.
MONITORINFO activeMonitor{};
activeMonitor.cbSize = sizeof(activeMonitor);
GetMonitorInfo(MonitorFromPoint(p, MONITOR_DEFAULTTONEAREST), &activeMonitor);
return activeMonitor;
}
// Method Description:
// - Get the monitor for a given HWND
// Arguments:
// - <none>
// Return Value:
// - The MONITORINFO for the given HWND
MONITORINFO IslandWindow::_getMonitorForWindow(HWND foregroundWindow)
{
// Get the monitor info for the window's current monitor.
MONITORINFO activeMonitor{};
activeMonitor.cbSize = sizeof(activeMonitor);
GetMonitorInfo(MonitorFromWindow(foregroundWindow, MONITOR_DEFAULTTONEAREST), &activeMonitor);
return activeMonitor;
}
// Method Description:
// - Based on the value in toMonitor, move the window to the monitor of the
// given HWND, the monitor of the mouse pointer, or just leave it where it is.
// Arguments:
// - oldForegroundWindow: when toMonitor is ToCurrent, we'll move to the monitor
// of this HWND. Otherwise, this param is ignored.
// - toMonitor: Controls which monitor we should move to.
// Return Value:
// - <none>
void IslandWindow::_moveToMonitor(HWND oldForegroundWindow, winrt::TerminalApp::MonitorBehavior toMonitor)
{
if (toMonitor == winrt::TerminalApp::MonitorBehavior::ToCurrent)
{
_moveToMonitorOf(oldForegroundWindow);
}
else if (toMonitor == winrt::TerminalApp::MonitorBehavior::ToMouse)
{
_moveToMonitorOfMouse();
}
}
// Method Description:
// - Move our window to the monitor the mouse is on.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::_moveToMonitorOfMouse()
{
_moveToMonitor(_getMonitorForCursor());
}
// Method Description:
// - Move our window to the monitor that the given HWND is on.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::_moveToMonitorOf(HWND foregroundWindow)
{
_moveToMonitor(_getMonitorForWindow(foregroundWindow));
}
// Method Description:
// - Move our window to the given monitor. This will do nothing if we're already
// on that monitor.
// - We'll retain the same relative position on the new monitor as we had on the
// old monitor.
// Arguments:
// - activeMonitor: the monitor to move to.
// Return Value:
// - <none>
void IslandWindow::_moveToMonitor(const MONITORINFO activeMonitor)
{
// Get the monitor info for the window's current monitor.
const auto currentMonitor = _getMonitorForWindow(GetHandle());
const til::rect currentRect{ currentMonitor.rcMonitor };
const til::rect activeRect{ activeMonitor.rcMonitor };
if (currentRect != activeRect)
{
const til::rect currentWindowRect{ GetWindowRect() };
const auto offset{ currentWindowRect.origin() - currentRect.origin() };
const auto newOrigin{ activeRect.origin() + offset };
SetWindowPos(GetHandle(),
0,
newOrigin.x,
newOrigin.y,
currentWindowRect.width(),
currentWindowRect.height(),
SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE);
// GH#10274, GH#10182: Re-evaluate the size of the quake window when we
// move to another monitor.
if (IsQuakeWindow())
{
_enterQuakeMode();
}
}
}
bool IslandWindow::IsQuakeWindow() const noexcept
{
return _isQuakeWindow;
}
void IslandWindow::IsQuakeWindow(bool isQuakeWindow) noexcept
{
if (_isQuakeWindow != isQuakeWindow)
{
_isQuakeWindow = isQuakeWindow;
// Don't enter quake mode if we don't have an HWND yet
if (IsQuakeWindow() && _window)
{
_enterQuakeMode();
}
}
}
void IslandWindow::SetAutoHideWindow(bool autoHideWindow) noexcept
{
_autoHideWindow = autoHideWindow;
}
// Method Description:
// - Enter quake mode for the monitor this window is currently on. This involves
// resizing it to the top half of the monitor.
// Arguments:
// - <none>
// Return Value:
// - <none>
void IslandWindow::_enterQuakeMode()
{
if (!_window)
{
return;
}
auto windowRect = GetWindowRect();
auto hmon = MonitorFromRect(&windowRect, MONITOR_DEFAULTTONEAREST);
// Get the size and position of the window that we should occupy
const auto newRect{ _getQuakeModeSize(hmon) };
SetWindowPos(GetHandle(),
HWND_TOP,
newRect.left,
newRect.top,
newRect.width(),
newRect.height(),
SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOACTIVATE);
}
// Method Description:
// - Get the size and position of the window that a "quake mode" should occupy
// on the given monitor.
// - The window will occupy the top half of the monitor.
// Arguments:
// - <none>
// Return Value:
// - <none>
til::rect IslandWindow::_getQuakeModeSize(HMONITOR hmon)
{
MONITORINFO nearestMonitorInfo;
UINT dpix = USER_DEFAULT_SCREEN_DPI;
UINT dpiy = USER_DEFAULT_SCREEN_DPI;
// If this fails, we'll use the default of 96. I think it can only fail for
// bad parameters, which we won't have, so no big deal.
LOG_IF_FAILED(GetDpiForMonitor(hmon, MDT_EFFECTIVE_DPI, &dpix, &dpiy));
nearestMonitorInfo.cbSize = sizeof(MONITORINFO);
// Get monitor dimensions:
GetMonitorInfo(hmon, &nearestMonitorInfo);
const til::size desktopDimensions{ (nearestMonitorInfo.rcWork.right - nearestMonitorInfo.rcWork.left),
(nearestMonitorInfo.rcWork.bottom - nearestMonitorInfo.rcWork.top) };
// If we just use rcWork by itself, we'll fail to account for the invisible
// space reserved for the resize handles. So retrieve that size here.
const til::size ncSize{ GetTotalNonClientExclusiveSize(dpix) };
const auto availableSpace = desktopDimensions + ncSize;
// GH#10201 - The borders are still visible in quake mode, so make us 1px
// smaller on either side to account for that, so they don't hang onto
// adjacent monitors.
const til::point origin{
::base::ClampSub(nearestMonitorInfo.rcWork.left, (ncSize.width / 2)) + 1,
(nearestMonitorInfo.rcWork.top)
};
const til::size dimensions{
availableSpace.width - 2,
availableSpace.height / 2
};
return { origin, dimensions };
}
void IslandWindow::HideWindow()
{
ShowWindow(GetHandle(), SW_HIDE);
}
void IslandWindow::SetMinimizeToNotificationAreaBehavior(bool MinimizeToNotificationArea) noexcept
{
_minimizeToNotificationArea = MinimizeToNotificationArea;
}
// Method Description:
// - Opens the window's system menu.
// - The system menu is the menu that opens when the user presses Alt+Space or
// right clicks on the title bar.
// - Before updating the menu, we update the buttons like "Maximize" and
// "Restore" so that they are grayed out depending on the window's state.
// Arguments:
// - cursorX: the cursor's X position in screen coordinates
// - cursorY: the cursor's Y position in screen coordinates
void IslandWindow::OpenSystemMenu(const std::optional<int> mouseX, const std::optional<int> mouseY) const noexcept
{
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
WINDOWPLACEMENT placement;
if (!GetWindowPlacement(_window.get(), &placement))
{
return;
}
const auto isMaximized = placement.showCmd == SW_SHOWMAXIMIZED;
// Update the options based on window state.
MENUITEMINFO mii;
mii.cbSize = sizeof(MENUITEMINFO);
mii.fMask = MIIM_STATE;
mii.fType = MFT_STRING;
auto setState = [&](UINT item, bool enabled) {
mii.fState = enabled ? MF_ENABLED : MF_DISABLED;
SetMenuItemInfo(systemMenu, item, FALSE, &mii);
};
setState(SC_RESTORE, isMaximized);
setState(SC_MOVE, !isMaximized);
setState(SC_SIZE, !isMaximized);
setState(SC_MINIMIZE, true);
setState(SC_MAXIMIZE, !isMaximized);
setState(SC_CLOSE, true);
SetMenuDefaultItem(systemMenu, UINT_MAX, FALSE);
int xPos;
int yPos;
if (mouseX && mouseY)
{
xPos = mouseX.value();
yPos = mouseY.value();
}
else
{
RECT windowPos;
::GetWindowRect(GetHandle(), &windowPos);
xPos = windowPos.left;
yPos = windowPos.top;
}
const auto ret = TrackPopupMenu(systemMenu, TPM_RETURNCMD, xPos, yPos, 0, _window.get(), nullptr);
if (ret != 0)
{
PostMessage(_window.get(), WM_SYSCOMMAND, ret, 0);
}
}
void IslandWindow::AddToSystemMenu(const winrt::hstring& itemLabel, winrt::delegate<void()> callback)
{
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
auto wID = _systemMenuNextItemId;
MENUITEMINFOW item;
item.cbSize = sizeof(MENUITEMINFOW);
item.fMask = MIIM_STATE | MIIM_ID | MIIM_STRING;
item.fState = MF_ENABLED;
item.wID = wID;
item.dwTypeData = const_cast<LPWSTR>(itemLabel.c_str());
item.cch = static_cast<UINT>(itemLabel.size());
if (LOG_LAST_ERROR_IF(!InsertMenuItemW(systemMenu, wID, FALSE, &item)))
{
return;
}
_systemMenuItems.insert({ wID, { itemLabel, callback } });
_systemMenuNextItemId++;
}
void IslandWindow::RemoveFromSystemMenu(const winrt::hstring& itemLabel)
{
const auto systemMenu = GetSystemMenu(_window.get(), FALSE);
auto itemCount = GetMenuItemCount(systemMenu);
if (LOG_LAST_ERROR_IF(itemCount == -1))
{
return;
}
auto it = std::find_if(_systemMenuItems.begin(), _systemMenuItems.end(), [&itemLabel](const std::pair<UINT, SystemMenuItemInfo>& elem) {
return elem.second.label == itemLabel;
});
if (it == _systemMenuItems.end())
{
return;
}
if (LOG_LAST_ERROR_IF(!DeleteMenu(systemMenu, it->first, MF_BYCOMMAND)))
{
return;
}
_systemMenuItems.erase(it->first);
}
void IslandWindow::_resetSystemMenu()
{
// GetSystemMenu(..., true) will revert the menu to the default state.
GetSystemMenu(_window.get(), TRUE);
}
void IslandWindow::UseDarkTheme(const bool v)
{
const BOOL attribute = v ? TRUE : FALSE;
std::ignore = DwmSetWindowAttribute(GetHandle(), DWMWA_USE_IMMERSIVE_DARK_MODE, &attribute, sizeof(attribute));
}
void IslandWindow::UseMica(const bool newValue, const double /*titlebarOpacity*/)
{
// This block of code enables Mica for our window. By all accounts, this
// version of the code will only work on Windows 11, SV2. There's a slightly
// different API surface for enabling Mica on Windows 11 22000.0.
//
// This API was only publicly supported as of Windows 11 SV2, 22621. Before
// that version, this API will just return an error and do nothing silently.
const int attribute = newValue ? DWMSBT_MAINWINDOW : DWMSBT_NONE;
std::ignore = DwmSetWindowAttribute(GetHandle(), DWMWA_SYSTEMBACKDROP_TYPE, &attribute, sizeof(attribute));
}
// Method Description:
// - This method is called when the window receives the WM_NCCREATE message.
// Return Value:
// - The value returned from the window proc.
[[nodiscard]] LRESULT IslandWindow::OnNcCreate(WPARAM wParam, LPARAM lParam) noexcept
{
const auto ret = BaseWindow::OnNcCreate(wParam, lParam);
if (!ret)
{
return FALSE;
}
// This is a hack to make the window borders dark instead of light.
// It must be done before WM_NCPAINT so that the borders are rendered with
// the correct theme.
// For more information, see GH#6620.
//
// Theoretically, we don't need this anymore, since _updateTheme will update
// the darkness of our window. However, we're keeping this call to prevent
// the window from appearing as a white rectangle for a frame before we load
// the rest of the settings.
UseDarkTheme(true);
return TRUE;
}