Leonard Hecker 5ddf429688 wip
2025-09-02 19:47:21 +02:00

1196 lines
38 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "HwndTerminal.hpp"
#include <DefaultSettings.h>
#include <windowsx.h>
#include "HwndTerminalAutomationPeer.hpp"
#include "../../cascadia/TerminalCore/Terminal.hpp"
#include "../../renderer/atlas/AtlasEngine.h"
#include "../../renderer/base/renderer.hpp"
#include "../../renderer/uia/UiaRenderer.hpp"
#include "../../types/viewport.cpp"
using namespace ::Microsoft::Console::VirtualTerminal;
using namespace ::Microsoft::Terminal::Core;
static LPCWSTR term_window_class = L"HwndTerminalClass";
STDMETHODIMP HwndTerminal::TsfDataProvider::QueryInterface(REFIID, void**) noexcept
{
return E_NOTIMPL;
}
ULONG STDMETHODCALLTYPE HwndTerminal::TsfDataProvider::AddRef() noexcept
{
return 1;
}
ULONG STDMETHODCALLTYPE HwndTerminal::TsfDataProvider::Release() noexcept
{
return 1;
}
HWND HwndTerminal::TsfDataProvider::GetHwnd()
{
return _terminal->GetHwnd();
}
RECT HwndTerminal::TsfDataProvider::GetViewport()
{
const auto hwnd = GetHwnd();
RECT rc;
GetClientRect(hwnd, &rc);
// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getclientrect
// > The left and top members are zero. The right and bottom members contain the width and height of the window.
// --> We can turn the client rect into a screen-relative rect by adding the left/top position.
ClientToScreen(hwnd, reinterpret_cast<POINT*>(&rc));
rc.right += rc.left;
rc.bottom += rc.top;
return rc;
}
RECT HwndTerminal::TsfDataProvider::GetCursorPosition()
{
// Convert from columns/rows to pixels.
til::point cursorPos;
til::size fontSize;
{
const auto lock = _terminal->_terminal->LockForReading();
cursorPos = _terminal->_terminal->GetCursorPosition(); // measured in terminal cells
fontSize = _terminal->_actualFont.GetSize(); // measured in pixels, not DIP
}
POINT ptSuggestion = {
.x = cursorPos.x * fontSize.width,
.y = cursorPos.y * fontSize.height,
};
ClientToScreen(GetHwnd(), &ptSuggestion);
// Final measurement should be in pixels
return {
.left = ptSuggestion.x,
.top = ptSuggestion.y,
.right = ptSuggestion.x + fontSize.width,
.bottom = ptSuggestion.y + fontSize.height,
};
}
void HwndTerminal::TsfDataProvider::HandleOutput(std::wstring_view text)
{
_terminal->_WriteTextToConnection(text);
}
Microsoft::Console::Render::Renderer* HwndTerminal::TsfDataProvider::GetRenderer()
{
return _terminal->_renderer.get();
}
// This magic flag is "documented" at https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301(v=vs.85).aspx
// "If the high-order bit is 1, the key is down; otherwise, it is up."
static constexpr short KeyPressed{ gsl::narrow_cast<short>(0x8000) };
static constexpr bool _IsMouseMessage(UINT uMsg)
{
return uMsg == WM_LBUTTONDOWN || uMsg == WM_LBUTTONUP || uMsg == WM_LBUTTONDBLCLK ||
uMsg == WM_MBUTTONDOWN || uMsg == WM_MBUTTONUP || uMsg == WM_MBUTTONDBLCLK ||
uMsg == WM_RBUTTONDOWN || uMsg == WM_RBUTTONUP || uMsg == WM_RBUTTONDBLCLK ||
uMsg == WM_MOUSEMOVE || uMsg == WM_MOUSEWHEEL || uMsg == WM_MOUSEHWHEEL;
}
LRESULT CALLBACK HwndTerminal::HwndTerminalWndProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam) noexcept
try
{
if (WM_NCCREATE == uMsg)
{
#pragma warning(suppress : 26490) // Win32 APIs can only store void*, have to use reinterpret_cast
auto cs = reinterpret_cast<CREATESTRUCT*>(lParam);
HwndTerminal* that = static_cast<HwndTerminal*>(cs->lpCreateParams);
that->_hwnd = wil::unique_hwnd(hwnd);
#pragma warning(suppress : 26490) // Win32 APIs can only store void*, have to use reinterpret_cast
SetWindowLongPtr(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(that));
return DefWindowProc(hwnd, WM_NCCREATE, wParam, lParam);
}
#pragma warning(suppress : 26490) // Win32 APIs can only store void*, have to use reinterpret_cast
const auto publicTerminal = reinterpret_cast<HwndTerminal*>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
if (publicTerminal)
{
if (_IsMouseMessage(uMsg))
{
if (publicTerminal->_CanSendVTMouseInput() && publicTerminal->_SendMouseEvent(uMsg, wParam, lParam))
{
// GH#6401: Capturing the mouse ensures that we get drag/release events
// even if the user moves outside the window.
// _SendMouseEvent returns false if the terminal's not in VT mode, so we'll
// fall through to release the capture.
switch (uMsg)
{
case WM_LBUTTONDOWN:
case WM_MBUTTONDOWN:
case WM_RBUTTONDOWN:
SetCapture(hwnd);
break;
case WM_LBUTTONUP:
case WM_MBUTTONUP:
case WM_RBUTTONUP:
ReleaseCapture();
break;
default:
break;
}
// Suppress all mouse events that made it into the terminal.
return 0;
}
}
switch (uMsg)
{
case WM_GETOBJECT:
if (lParam == UiaRootObjectId)
{
return UiaReturnRawElementProvider(hwnd, wParam, lParam, publicTerminal->_GetUiaProvider());
}
break;
case WM_LBUTTONDOWN:
LOG_IF_FAILED(publicTerminal->_StartSelection(lParam));
return 0;
case WM_LBUTTONUP:
publicTerminal->_singleClickTouchdownPos = std::nullopt;
[[fallthrough]];
case WM_MBUTTONUP:
case WM_RBUTTONUP:
ReleaseCapture();
break;
case WM_MOUSEMOVE:
if (WI_IsFlagSet(wParam, MK_LBUTTON))
{
LOG_IF_FAILED(publicTerminal->_MoveSelection(lParam));
return 0;
}
break;
case WM_RBUTTONDOWN:
try
{
if (publicTerminal->_terminal)
{
const auto lock = publicTerminal->_terminal->LockForWriting();
if (publicTerminal->_terminal->IsSelectionActive())
{
const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false, false, true, true);
LOG_IF_FAILED(publicTerminal->_CopyTextToSystemClipboard(bufferData.plainText, bufferData.html, bufferData.rtf));
publicTerminal->_ClearSelection();
return 0;
}
}
publicTerminal->_PasteTextFromClipboard();
return 0;
}
CATCH_LOG();
case WM_DESTROY:
// Release Terminal's hwnd so Teardown doesn't try to destroy it again
publicTerminal->_hwnd.release();
publicTerminal->Teardown();
return 0;
default:
break;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return 0;
}
static bool RegisterTermClass(HINSTANCE hInstance) noexcept
{
WNDCLASSW wc;
if (GetClassInfoW(hInstance, term_window_class, &wc))
{
return true;
}
wc.style = 0;
wc.lpfnWndProc = HwndTerminal::HwndTerminalWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = nullptr;
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = nullptr;
wc.lpszMenuName = nullptr;
wc.lpszClassName = term_window_class;
return RegisterClassW(&wc) != 0;
}
HwndTerminal::HwndTerminal(HWND parentHwnd) noexcept :
_desiredFont{ L"Consolas", 0, DEFAULT_FONT_WEIGHT, 14, CP_UTF8 },
_actualFont{ L"Consolas", 0, DEFAULT_FONT_WEIGHT, { 0, 14 }, CP_UTF8, false },
_uiaProvider{ nullptr },
_currentDpi{ USER_DEFAULT_SCREEN_DPI },
_pfnWriteCallback{ nullptr },
_multiClickTime{ 500 } // this will be overwritten by the windows system double-click time
{
auto hInstance = wil::GetModuleInstanceHandle();
if (RegisterTermClass(hInstance))
{
CreateWindowExW(
0,
term_window_class,
nullptr,
WS_CHILD |
WS_CLIPCHILDREN |
WS_CLIPSIBLINGS |
WS_VISIBLE,
0,
0,
0,
0,
parentHwnd,
nullptr,
hInstance,
this);
}
}
HwndTerminal::~HwndTerminal()
{
Teardown();
}
HRESULT HwndTerminal::Initialize()
{
_terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
const auto lock = _terminal->LockForWriting();
auto& renderSettings = _terminal->GetRenderSettings();
renderSettings.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, RGB(12, 12, 12));
renderSettings.SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, RGB(204, 204, 204));
_renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(renderSettings, _terminal.get());
auto engine = std::make_unique<::Microsoft::Console::Render::AtlasEngine>();
RETURN_IF_FAILED(engine->SetHwnd(_hwnd.get()));
_renderer->AddRenderEngine(engine.get());
_UpdateFont(USER_DEFAULT_SCREEN_DPI);
RECT windowRect;
GetWindowRect(_hwnd.get(), &windowRect);
const til::size windowSize{ windowRect.right - windowRect.left, windowRect.bottom - windowRect.top };
// Fist set up the dx engine with the window size in pixels.
// Then, using the font, get the number of characters that can fit.
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
RETURN_IF_FAILED(engine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
_renderEngine = std::move(engine);
_terminal->Create({ 80, 25 }, 0, 9001, *_renderer);
_terminal->SetWriteInputCallback([=](std::wstring_view input) noexcept { _WriteTextToConnection(input); });
_terminal->SetCopyToClipboardCallback([=](wil::zwstring_view text) noexcept { _CopyTextToSystemClipboard(text, {}, {}); });
_renderer->EnablePainting();
_multiClickTime = std::chrono::milliseconds{ GetDoubleClickTime() };
return S_OK;
}
void HwndTerminal::Teardown() noexcept
try
{
// As a rule, detach resources from the Terminal before shutting them down.
// This ensures that teardown is reentrant.
_tsfHandle = {};
// Shut down the renderer (and therefore the thread) before we implode
_renderer.reset();
_renderEngine.reset();
// These two callbacks have a dangling reference to `this`; let's just clear them
_terminal->SetWriteInputCallback(nullptr);
_terminal->SetCopyToClipboardCallback(nullptr);
if (auto localHwnd{ _hwnd.release() })
{
// If we're being called through WM_DESTROY, we won't get here (hwnd is already released)
// If we're not, we may end up in Teardown _again_... but by the time we do, all other
// resources have been released and will not be released again.
DestroyWindow(localHwnd);
}
}
CATCH_LOG();
void HwndTerminal::RegisterScrollCallback(std::function<void(int, int, int)> callback)
{
if (!_terminal)
{
return;
}
_terminal->SetScrollPositionChangedCallback(callback);
}
void HwndTerminal::_WriteTextToConnection(const std::wstring_view input) noexcept
{
if (input.empty() || !_pfnWriteCallback)
{
return;
}
try
{
auto callingText{ wil::make_cotaskmem_string(input.data(), input.size()) };
_pfnWriteCallback(callingText.release());
}
CATCH_LOG();
}
void HwndTerminal::RegisterWriteCallback(const void _stdcall callback(wchar_t*))
{
_pfnWriteCallback = callback;
}
::Microsoft::Console::Render::IRenderData* HwndTerminal::GetRenderData() const noexcept
{
return _terminal.get();
}
HWND HwndTerminal::GetHwnd() const noexcept
{
return _hwnd.get();
}
void HwndTerminal::_UpdateFont(int newDpi)
{
if (!_terminal)
{
return;
}
_currentDpi = newDpi;
// TODO: MSFT:20895307 If the font doesn't exist, this doesn't
// actually fail. We need a way to gracefully fallback.
_renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont);
}
IRawElementProviderSimple* HwndTerminal::_GetUiaProvider() noexcept
{
// If TermControlUiaProvider throws during construction,
// we don't want to try constructing an instance again and again.
if (!_uiaProvider)
{
try
{
if (!_terminal)
{
return nullptr;
}
LOG_IF_FAILED(::Microsoft::WRL::MakeAndInitialize<HwndTerminalAutomationPeer>(&_uiaProvider, this->GetRenderData(), this));
_uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(_uiaProvider.Get());
LOG_IF_FAILED(_uiaEngine->Enable());
const auto lock = _terminal->LockForWriting();
_renderer->AddRenderEngine(_uiaEngine.get());
}
catch (...)
{
LOG_HR(wil::ResultFromCaughtException());
_uiaProvider = nullptr;
}
}
return _uiaProvider.Get();
}
HRESULT HwndTerminal::Refresh(const til::size windowSize, _Out_ til::size* dimensions)
{
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _terminal);
RETURN_HR_IF_NULL(E_INVALIDARG, dimensions);
const auto lock = _terminal->LockForWriting();
_terminal->ClearSelection();
RETURN_IF_FAILED(_renderEngine->SetWindowSize(windowSize));
// Invalidate everything
_renderer->TriggerRedrawAll();
// Convert our new dimensions to characters
const auto viewInPixels = Viewport::FromDimensions({}, windowSize);
const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);
// Guard against resizing the window to 0 columns/rows, which the text buffer classes don't really support.
auto size = vp.Dimensions();
size.width = std::max(size.width, 1);
size.height = std::max(size.height, 1);
// If this function succeeds with S_FALSE, then the terminal didn't
// actually change size. No need to notify the connection of this
// no-op.
// TODO: MSFT:20642295 Resizing the buffer will corrupt it
// I believe we'll need support for CSI 2J, and additionally I think
// we're resetting the viewport to the top
RETURN_IF_FAILED(_terminal->UserResize(size));
dimensions->width = size.width;
dimensions->height = size.height;
return S_OK;
}
void HwndTerminal::SendOutput(std::wstring_view data)
{
if (!_terminal)
{
return;
}
const auto lock = _terminal->LockForWriting();
_terminal->Write(data);
}
HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal)
{
auto publicTerminal = std::make_unique<HwndTerminal>(parentHwnd);
RETURN_IF_FAILED(publicTerminal->Initialize());
*hwnd = publicTerminal->GetHwnd();
*terminal = publicTerminal.release();
return S_OK;
}
void _stdcall TerminalRegisterScrollCallback(void* terminal, void __stdcall callback(int, int, int))
try
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->RegisterScrollCallback(callback);
}
CATCH_LOG()
void _stdcall TerminalRegisterWriteCallback(void* terminal, const void __stdcall callback(wchar_t*))
try
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->RegisterWriteCallback(callback);
}
CATCH_LOG()
void _stdcall TerminalSendOutput(void* terminal, LPCWSTR data)
try
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->SendOutput(data);
}
CATCH_LOG()
/// <summary>
/// Triggers a terminal resize using the new width and height in pixel.
/// </summary>
/// <param name="terminal">Terminal pointer.</param>
/// <param name="width">New width of the terminal in pixels.</param>
/// <param name="height">New height of the terminal in pixels</param>
/// <param name="dimensions">Out parameter containing the columns and rows that fit the new size.</param>
/// <returns>HRESULT of the attempted resize.</returns>
HRESULT _stdcall TerminalTriggerResize(_In_ void* terminal, _In_ til::CoordType width, _In_ til::CoordType height, _Out_ til::size* dimensions)
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
LOG_IF_WIN32_BOOL_FALSE(SetWindowPos(
publicTerminal->GetHwnd(),
nullptr,
0,
0,
static_cast<int>(width),
static_cast<int>(height),
0));
const til::size windowSize{ width, height };
return publicTerminal->Refresh(windowSize, dimensions);
}
/// <summary>
/// Helper method for resizing the terminal using character column and row counts
/// </summary>
/// <param name="terminal">Pointer to the terminal object.</param>
/// <param name="dimensionsInCharacters">New terminal size in row and column count.</param>
/// <param name="dimensionsInPixels">Out parameter with the new size of the renderer.</param>
/// <returns>HRESULT of the attempted resize.</returns>
HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ void* terminal, _In_ til::size dimensionsInCharacters, _Out_ til::size* dimensionsInPixels)
try
{
RETURN_HR_IF_NULL(E_INVALIDARG, dimensionsInPixels);
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
Viewport viewInPixels;
{
const auto viewInCharacters = Viewport::FromDimensions({}, dimensionsInCharacters);
const auto lock = publicTerminal->_terminal->LockForReading();
viewInPixels = publicTerminal->_renderEngine->GetViewportInPixels(viewInCharacters);
}
dimensionsInPixels->width = viewInPixels.Width();
dimensionsInPixels->height = viewInPixels.Height();
til::size unused;
return TerminalTriggerResize(terminal, viewInPixels.Width(), viewInPixels.Height(), &unused);
}
CATCH_RETURN()
/// <summary>
/// Calculates the amount of rows and columns that fit in the provided width and height.
/// </summary>
/// <param name="terminal">Terminal pointer</param>
/// <param name="width">Width of the terminal area to calculate.</param>
/// <param name="height">Height of the terminal area to calculate.</param>
/// <param name="dimensions">Out parameter containing the columns and rows that fit the new size.</param>
/// <returns>HRESULT of the calculation.</returns>
HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ til::CoordType width, _In_ til::CoordType height, _Out_ til::size* dimensions)
try
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
const auto viewInPixels = Viewport::FromDimensions({}, { width, height });
const auto lock = publicTerminal->_terminal->LockForReading();
const auto viewInCharacters = publicTerminal->_renderEngine->GetViewportInCharacters(viewInPixels);
dimensions->width = viewInCharacters.Width();
dimensions->height = viewInCharacters.Height();
return S_OK;
}
CATCH_RETURN()
void _stdcall TerminalDpiChanged(void* terminal, int newDpi)
try
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
const auto lock = publicTerminal->_terminal->LockForWriting();
publicTerminal->_UpdateFont(newDpi);
}
CATCH_LOG()
void _stdcall TerminalUserScroll(void* terminal, int viewTop)
try
{
if (const auto publicTerminal = static_cast<const HwndTerminal*>(terminal); publicTerminal && publicTerminal->_terminal)
{
const auto lock = publicTerminal->_terminal->LockForWriting();
const auto current = publicTerminal->_terminal->GetScrollOffset();
publicTerminal->_terminal->UserScrollViewport({ current.x, viewTop });
}
}
CATCH_LOG()
const unsigned int HwndTerminal::_NumberOfClicks(til::point point, std::chrono::steady_clock::time_point timestamp) noexcept
{
// if click occurred at a different location or past the multiClickTimer...
const auto delta{ timestamp - _lastMouseClickTimestamp };
if (point != _lastMouseClickPos || delta > _multiClickTime)
{
// exit early. This is a single click.
_multiClickCounter = 1;
}
else
{
_multiClickCounter++;
}
return _multiClickCounter;
}
HRESULT HwndTerminal::_StartSelection(LPARAM lParam) noexcept
try
{
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _terminal);
const til::point cursorPosition{
GET_X_LPARAM(lParam),
GET_Y_LPARAM(lParam),
};
const auto lock = _terminal->LockForWriting();
const auto altPressed = GetKeyState(VK_MENU) < 0;
const til::size fontSize{ this->_actualFont.GetSize() };
this->_terminal->SetBlockSelection(altPressed);
const auto clickCount{ _NumberOfClicks(cursorPosition, std::chrono::steady_clock::now()) };
// This formula enables the number of clicks to cycle properly between single-, double-, and triple-click.
// To increase the number of acceptable click states, simply increment MAX_CLICK_COUNT and add another if-statement
const unsigned int MAX_CLICK_COUNT = 3;
const auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ((clickCount + MAX_CLICK_COUNT - 1) % MAX_CLICK_COUNT) + 1 : clickCount;
if (multiClickMapper == 3)
{
_terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansion::Line);
}
else if (multiClickMapper == 2)
{
_terminal->MultiClickSelection(cursorPosition / fontSize, ::Terminal::SelectionExpansion::Word);
}
else
{
this->_terminal->ClearSelection();
_singleClickTouchdownPos = cursorPosition;
_lastMouseClickTimestamp = std::chrono::steady_clock::now();
_lastMouseClickPos = cursorPosition;
}
this->_renderer->TriggerSelection();
return S_OK;
}
CATCH_RETURN();
HRESULT HwndTerminal::_MoveSelection(LPARAM lParam) noexcept
try
{
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _terminal);
const til::point cursorPosition{
GET_X_LPARAM(lParam),
GET_Y_LPARAM(lParam),
};
const auto lock = _terminal->LockForWriting();
const til::size fontSize{ this->_actualFont.GetSize() };
RETURN_HR_IF(E_NOT_VALID_STATE, fontSize.area() == 0); // either dimension = 0, area == 0
// This is a copy of ControlInteractivity::PointerMoved
if (_singleClickTouchdownPos)
{
const auto touchdownPoint = *_singleClickTouchdownPos;
const auto dx = cursorPosition.x - touchdownPoint.x;
const auto dy = cursorPosition.y - touchdownPoint.y;
const auto w = fontSize.width;
const auto distanceSquared = dx * dx + dy * dy;
const auto maxDistanceSquared = w * w / 16; // (w / 4)^2
if (distanceSquared >= maxDistanceSquared)
{
_terminal->SetSelectionAnchor(touchdownPoint / fontSize);
// stop tracking the touchdown point
_singleClickTouchdownPos = std::nullopt;
}
}
this->_terminal->SetSelectionEnd(cursorPosition / fontSize);
this->_renderer->TriggerSelection();
return S_OK;
}
CATCH_RETURN();
void HwndTerminal::_ClearSelection()
{
if (!_terminal)
{
return;
}
_terminal->ClearSelection();
_renderer->TriggerSelection();
}
void _stdcall TerminalClearSelection(void* terminal)
try
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
const auto lock = publicTerminal->_terminal->LockForWriting();
publicTerminal->_ClearSelection();
}
CATCH_LOG()
bool _stdcall TerminalIsSelectionActive(void* terminal)
try
{
if (const auto publicTerminal = static_cast<const HwndTerminal*>(terminal); publicTerminal && publicTerminal->_terminal)
{
const auto lock = publicTerminal->_terminal->LockForReading();
return publicTerminal->_terminal->IsSelectionActive();
}
return false;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return false;
}
// Returns the selected text in the terminal.
const wchar_t* _stdcall TerminalGetSelection(void* terminal)
try
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
if (!publicTerminal || !publicTerminal->_terminal)
{
return nullptr;
}
std::wstring selectedText;
{
const auto lock = publicTerminal->_terminal->LockForWriting();
auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false);
selectedText = std::move(bufferData.plainText);
publicTerminal->_ClearSelection();
}
auto returnText = wil::make_cotaskmem_string_nothrow(selectedText.c_str());
return returnText.release();
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return nullptr;
}
static ControlKeyStates getControlKeyState() noexcept
{
struct KeyModifier
{
int vkey;
ControlKeyStates flags;
};
constexpr std::array<KeyModifier, 5> modifiers{ {
{ VK_RMENU, ControlKeyStates::RightAltPressed },
{ VK_LMENU, ControlKeyStates::LeftAltPressed },
{ VK_RCONTROL, ControlKeyStates::RightCtrlPressed },
{ VK_LCONTROL, ControlKeyStates::LeftCtrlPressed },
{ VK_SHIFT, ControlKeyStates::ShiftPressed },
} };
ControlKeyStates flags;
for (const auto& mod : modifiers)
{
const auto state = GetKeyState(mod.vkey);
const auto isDown = state < 0;
if (isDown)
{
flags |= mod.flags;
}
}
return flags;
}
bool HwndTerminal::_CanSendVTMouseInput() const noexcept
{
// Only allow the transit of mouse events if shift isn't pressed.
const auto shiftPressed = GetKeyState(VK_SHIFT) < 0;
const auto lock = _terminal->LockForReading();
return !shiftPressed && _focused && _terminal && _terminal->IsTrackingMouseInput();
}
bool HwndTerminal::_SendMouseEvent(UINT uMsg, WPARAM wParam, LPARAM lParam) noexcept
try
{
if (!_terminal)
{
return false;
}
til::point cursorPosition{
GET_X_LPARAM(lParam),
GET_Y_LPARAM(lParam),
};
const til::size fontSize{ this->_actualFont.GetSize() };
short wheelDelta{ 0 };
if (uMsg == WM_MOUSEWHEEL || uMsg == WM_MOUSEHWHEEL)
{
wheelDelta = HIWORD(wParam);
// If it's a *WHEEL event, it's in screen coordinates, not window (?!)
ScreenToClient(_hwnd.get(), cursorPosition.as_win32_point());
}
const Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state{
WI_IsFlagSet(GetKeyState(VK_LBUTTON), KeyPressed),
WI_IsFlagSet(GetKeyState(VK_MBUTTON), KeyPressed),
WI_IsFlagSet(GetKeyState(VK_RBUTTON), KeyPressed)
};
TerminalInput::OutputType out;
{
const auto lock = _terminal->LockForReading();
out = _terminal->SendMouseEvent(cursorPosition / fontSize, uMsg, getControlKeyState(), wheelDelta, state);
}
if (out)
{
_WriteTextToConnection(*out);
return true;
}
return false;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return false;
}
void HwndTerminal::_SendKeyEvent(WORD vkey, WORD scanCode, WORD flags, bool keyDown) noexcept
try
{
if (!_terminal)
{
return;
}
auto modifiers = getControlKeyState();
if (WI_IsFlagSet(flags, ENHANCED_KEY))
{
modifiers |= ControlKeyStates::EnhancedKey;
}
if (vkey && keyDown && _uiaProvider)
{
_uiaProvider->RecordKeyEvent(vkey);
}
TerminalInput::OutputType out;
{
const auto lock = _terminal->LockForReading();
out = _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown);
}
if (out)
{
_WriteTextToConnection(*out);
}
}
CATCH_LOG();
void HwndTerminal::_SendCharEvent(wchar_t ch, WORD scanCode, WORD flags) noexcept
try
{
if (!_terminal)
{
return;
}
TerminalInput::OutputType out;
{
const auto lock = _terminal->LockForWriting();
if (_terminal->IsSelectionActive())
{
_ClearSelection();
if (ch == UNICODE_ESC)
{
// ESC should clear any selection before it triggers input.
// Other characters pass through.
return;
}
}
if (ch == UNICODE_TAB)
{
// TAB was handled as a keydown event (cf. Terminal::SendKeyEvent)
return;
}
auto modifiers = getControlKeyState();
if (WI_IsFlagSet(flags, ENHANCED_KEY))
{
modifiers |= ControlKeyStates::EnhancedKey;
}
out = _terminal->SendCharEvent(ch, scanCode, modifiers);
}
if (out)
{
_WriteTextToConnection(*out);
}
}
CATCH_LOG();
void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, WORD flags, bool keyDown)
try
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->_SendKeyEvent(vkey, scanCode, flags, keyDown);
}
CATCH_LOG()
void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode, WORD flags)
try
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->_SendCharEvent(ch, scanCode, flags);
}
CATCH_LOG()
void _stdcall DestroyTerminal(void* terminal)
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
delete publicTerminal;
}
// Updates the terminal font type, size, color, as well as the background/foreground colors to a specified theme.
void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, til::CoordType fontSize, int newDpi)
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
if (!publicTerminal || !publicTerminal->_terminal)
{
return;
}
{
const auto lock = publicTerminal->_terminal->LockForWriting();
auto& renderSettings = publicTerminal->_terminal->GetRenderSettings();
renderSettings.SetColorTableEntry(TextColor::DEFAULT_FOREGROUND, theme.DefaultForeground);
renderSettings.SetColorTableEntry(TextColor::DEFAULT_BACKGROUND, theme.DefaultBackground);
renderSettings.SetColorTableEntry(TextColor::SELECTION_BACKGROUND, theme.DefaultSelectionBackground);
// Set the font colors
for (size_t tableIndex = 0; tableIndex < 16; tableIndex++)
{
// It's using gsl::at to check the index is in bounds, but the analyzer still calls this array-to-pointer-decay
GSL_SUPPRESS(bounds.3)
renderSettings.SetColorTableEntry(tableIndex, gsl::at(theme.ColorTable, tableIndex));
}
// Save these values as the new default render settings.
renderSettings.SaveDefaultSettings();
publicTerminal->_terminal->SetCursorStyle(static_cast<Microsoft::Console::VirtualTerminal::DispatchTypes::CursorStyle>(theme.CursorStyle));
publicTerminal->_desiredFont = { fontFamily, 0, DEFAULT_FONT_WEIGHT, static_cast<float>(fontSize), CP_UTF8 };
publicTerminal->_desiredFont.SetEnableBuiltinGlyphs(true);
publicTerminal->_UpdateFont(newDpi);
}
// When the font changes the terminal dimensions need to be recalculated since the available row and column
// space will have changed.
RECT windowRect;
GetWindowRect(publicTerminal->_hwnd.get(), &windowRect);
til::size dimensions;
const til::size windowSize{ windowRect.right - windowRect.left, windowRect.bottom - windowRect.top };
publicTerminal->Refresh(windowSize, &dimensions);
}
void _stdcall TerminalBlinkCursor(void* terminal)
try
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
if (!publicTerminal || !publicTerminal->_terminal)
{
return;
}
const auto lock = publicTerminal->_terminal->LockForWriting();
publicTerminal->_terminal->BlinkCursor();
}
CATCH_LOG()
void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible)
try
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
if (!publicTerminal || !publicTerminal->_terminal)
{
return;
}
const auto lock = publicTerminal->_terminal->LockForWriting();
publicTerminal->_terminal->SetCursorOn(visible);
}
CATCH_LOG()
void __stdcall TerminalSetFocus(void* terminal)
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->_focused = true;
if (auto uiaEngine = publicTerminal->_uiaEngine.get())
{
LOG_IF_FAILED(uiaEngine->Enable());
}
publicTerminal->_FocusTSF();
}
void HwndTerminal::_FocusTSF() noexcept
{
if (!_tsfHandle)
{
_tsfHandle = Microsoft::Console::TSF::Handle::Create();
_tsfHandle.AssociateFocus(&_tsfDataProvider);
}
}
void __stdcall TerminalKillFocus(void* terminal)
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->_focused = false;
if (auto uiaEngine = publicTerminal->_uiaEngine.get())
{
LOG_IF_FAILED(uiaEngine->Disable());
}
}
// Routine Description:
// - Copies the text given onto the global system clipboard.
// Arguments:
// - text - selected text in plain-text format
// - htmlData - selected text in HTML format
// - rtfData - selected text in RTF format
HRESULT HwndTerminal::_CopyTextToSystemClipboard(wil::zwstring_view text, wil::zstring_view htmlData, wil::zstring_view rtfData) const
try
{
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, _terminal);
// allocate the final clipboard data
const auto cchNeeded = text.size() + 1;
const auto cbNeeded = sizeof(wchar_t) * cchNeeded;
wil::unique_hglobal globalHandle(GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, cbNeeded));
RETURN_LAST_ERROR_IF_NULL(globalHandle.get());
auto pwszClipboard = static_cast<PWSTR>(GlobalLock(globalHandle.get()));
RETURN_LAST_ERROR_IF_NULL(pwszClipboard);
// The pattern gets a bit strange here because there's no good wil built-in for global lock of this type.
// Try to copy then immediately unlock. Don't throw until after (so the hglobal won't be freed until we unlock).
const auto hr = StringCchCopyW(pwszClipboard, cchNeeded, text.data());
GlobalUnlock(globalHandle.get());
RETURN_IF_FAILED(hr);
// Set global data to clipboard
RETURN_LAST_ERROR_IF(!OpenClipboard(_hwnd.get()));
{ // Clipboard Scope
auto clipboardCloser = wil::scope_exit([]() {
LOG_LAST_ERROR_IF(!CloseClipboard());
});
RETURN_LAST_ERROR_IF(!EmptyClipboard());
RETURN_LAST_ERROR_IF_NULL(SetClipboardData(CF_UNICODETEXT, globalHandle.get()));
if (!htmlData.empty())
{
RETURN_IF_FAILED(_CopyToSystemClipboard(htmlData, L"HTML Format"));
}
if (!rtfData.empty())
{
RETURN_IF_FAILED(_CopyToSystemClipboard(rtfData, L"Rich Text Format"));
}
}
// only free if we failed.
// the memory has to remain allocated if we successfully placed it on the clipboard.
// Releasing the smart pointer will leave it allocated as we exit scope.
globalHandle.release();
return S_OK;
}
CATCH_RETURN()
// Routine Description:
// - Copies the given string onto the global system clipboard in the specified format
// Arguments:
// - stringToCopy - The string to copy
// - lpszFormat - the name of the format
HRESULT HwndTerminal::_CopyToSystemClipboard(wil::zstring_view stringToCopy, LPCWSTR lpszFormat) const
{
const auto cbData = stringToCopy.size() + 1; // +1 for '\0'
if (cbData)
{
wil::unique_hglobal globalHandleData(GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, cbData));
RETURN_LAST_ERROR_IF_NULL(globalHandleData.get());
auto pszClipboardHTML = static_cast<PSTR>(GlobalLock(globalHandleData.get()));
RETURN_LAST_ERROR_IF_NULL(pszClipboardHTML);
// The pattern gets a bit strange here because there's no good wil built-in for global lock of this type.
// Try to copy then immediately unlock. Don't throw until after (so the hglobal won't be freed until we unlock).
const auto hr2 = StringCchCopyA(pszClipboardHTML, cbData, stringToCopy.data());
GlobalUnlock(globalHandleData.get());
RETURN_IF_FAILED(hr2);
const auto CF_FORMAT = RegisterClipboardFormatW(lpszFormat);
RETURN_LAST_ERROR_IF(0 == CF_FORMAT);
RETURN_LAST_ERROR_IF_NULL(SetClipboardData(CF_FORMAT, globalHandleData.get()));
// only free if we failed.
// the memory has to remain allocated if we successfully placed it on the clipboard.
// Releasing the smart pointer will leave it allocated as we exit scope.
globalHandleData.release();
}
return S_OK;
}
void HwndTerminal::_PasteTextFromClipboard() noexcept
{
// Get paste data from clipboard
if (!OpenClipboard(_hwnd.get()))
{
return;
}
auto ClipboardDataHandle = GetClipboardData(CF_UNICODETEXT);
if (ClipboardDataHandle == nullptr)
{
CloseClipboard();
return;
}
if (const auto pwstr = static_cast<PCWCH>(GlobalLock(ClipboardDataHandle)))
{
_WriteTextToConnection(pwstr);
}
GlobalUnlock(ClipboardDataHandle);
CloseClipboard();
}
til::size HwndTerminal::GetFontSize() const noexcept
{
return _actualFont.GetSize();
}
til::rect HwndTerminal::GetBounds() const noexcept
{
til::rect windowRect;
GetWindowRect(_hwnd.get(), windowRect.as_win32_rect());
return windowRect;
}
til::rect HwndTerminal::GetPadding() const noexcept
{
return {};
}
void HwndTerminal::ChangeViewport(const til::inclusive_rect& NewWindow)
{
if (!_terminal)
{
return;
}
const auto lock = _terminal->LockForWriting();
_terminal->UserScrollViewport({ NewWindow.left, NewWindow.top });
}
HRESULT HwndTerminal::GetHostUiaProvider(IRawElementProviderSimple** provider) noexcept
{
return UiaHostProviderFromHwnd(_hwnd.get(), provider);
}