add wpf control (#2004)

This adds the WPF control to our project, courtesy of the Visual Studio team.
It re-hosts the Terminal Control components inside a reusable WPF adapter so it can be composed onto C# type surfaces like Visual Studio requires.
This commit is contained in:
Zoey Riordan 2019-10-11 21:02:09 +00:00 committed by Michael Niksa
parent 7b23d8e66c
commit b9233c03d1
29 changed files with 1832 additions and 63 deletions

File diff suppressed because it is too large Load Diff

View File

@ -69,8 +69,6 @@ public:
Microsoft::Console::Render::IRenderTarget& renderTarget);
TextBuffer(const TextBuffer& a) = delete;
~TextBuffer() = default;
// Used for duplicating properties to another text buffer
void CopyProperties(const TextBuffer& OtherBuffer) noexcept;

View File

@ -0,0 +1,393 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "HwndTerminal.hpp"
#include <DefaultSettings.h>
#include "../../renderer/base/Renderer.hpp"
#include "../../renderer/dx/DxRenderer.hpp"
#include "../../cascadia/TerminalCore/Terminal.hpp"
#include "../../types/viewport.cpp"
#include "../../types/inc/GlyphWidth.hpp"
using namespace ::Microsoft::Terminal::Core;
static LPCWSTR term_window_class = L"HwndTerminalClass";
static LRESULT CALLBACK HwndTerminalWndProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam) noexcept
{
return DefWindowProcW(hwnd, uMsg, wParam, lParam);
}
static bool RegisterTermClass(HINSTANCE hInstance) noexcept
{
WNDCLASSW wc;
if (GetClassInfoW(hInstance, term_window_class, &wc))
{
return true;
}
wc.style = 0;
wc.lpfnWndProc = 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) :
_desiredFont{ DEFAULT_FONT_FACE.c_str(), 0, 10, { 0, 14 }, CP_UTF8 },
_actualFont{ DEFAULT_FONT_FACE.c_str(), 0, 10, { 0, 14 }, CP_UTF8, false }
{
HINSTANCE hInstance = wil::GetModuleInstanceHandle();
if (RegisterTermClass(hInstance))
{
_hwnd = wil::unique_hwnd(CreateWindowExW(
0,
term_window_class,
nullptr,
WS_CHILD |
WS_CLIPCHILDREN |
WS_CLIPSIBLINGS |
WS_VISIBLE,
0,
0,
0,
0,
parentHwnd,
nullptr,
hInstance,
nullptr));
}
}
HRESULT HwndTerminal::Initialize()
{
_terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>();
auto* const localPointerToThread = renderThread.get();
_renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread));
RETURN_HR_IF_NULL(E_POINTER, localPointerToThread);
RETURN_IF_FAILED(localPointerToThread->Initialize(_renderer.get()));
auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
RETURN_IF_FAILED(dxEngine->SetHwnd(_hwnd.get()));
RETURN_IF_FAILED(dxEngine->Enable());
_renderer->AddRenderEngine(dxEngine.get());
const auto pfn = std::bind(&::Microsoft::Console::Render::Renderer::IsGlyphWideByFont, _renderer.get(), std::placeholders::_1);
SetGlyphWidthFallback(pfn);
_UpdateFont(USER_DEFAULT_SCREEN_DPI);
RECT windowRect;
GetWindowRect(_hwnd.get(), &windowRect);
const COORD windowSize{ gsl::narrow<short>(windowRect.right - windowRect.left), gsl::narrow<short>(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(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
_renderEngine = std::move(dxEngine);
_terminal->SetBackgroundCallback([](auto) {});
_terminal->Create(COORD{ 80, 25 }, 1000, *_renderer);
_terminal->SetDefaultBackground(RGB(5, 27, 80));
_terminal->SetDefaultForeground(RGB(255, 255, 255));
localPointerToThread->EnablePainting();
return S_OK;
}
void HwndTerminal::RegisterScrollCallback(std::function<void(int, int, int)> callback)
{
_terminal->SetScrollPositionChangedCallback(callback);
}
void HwndTerminal::RegisterWriteCallback(const void _stdcall callback(wchar_t*))
{
_terminal->SetWriteInputCallback([=](std::wstring & input) noexcept {
const wchar_t* text = input.c_str();
const size_t textChars = wcslen(text) + 1;
const size_t textBytes = textChars * sizeof(wchar_t);
wchar_t* callingText = nullptr;
callingText = static_cast<wchar_t*>(::CoTaskMemAlloc(textBytes));
if (callingText == nullptr)
{
callback(nullptr);
}
else
{
wcscpy_s(callingText, textChars, text);
callback(callingText);
}
});
}
void HwndTerminal::_UpdateFont(int newDpi)
{
auto lock = _terminal->LockForWriting();
// 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);
}
HRESULT HwndTerminal::Refresh(const SIZE windowSize, _Out_ COORD* dimensions)
{
RETURN_HR_IF_NULL(E_INVALIDARG, dimensions);
auto lock = _terminal->LockForWriting();
RETURN_IF_FAILED(_renderEngine->SetWindowSize(windowSize));
// Invalidate everything
_renderer->TriggerRedrawAll();
// Convert our new dimensions to characters
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 },
{ gsl::narrow<short>(windowSize.cx), gsl::narrow<short>(windowSize.cy) });
const auto vp = _renderEngine->GetViewportInCharacters(viewInPixels);
// 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({ vp.Width(), vp.Height() }));
dimensions->X = vp.Width();
dimensions->Y = vp.Height();
return S_OK;
}
void HwndTerminal::SendOutput(std::wstring_view data)
{
_terminal->Write(data);
}
HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal)
{
auto _terminal = std::make_unique<HwndTerminal>(parentHwnd);
RETURN_IF_FAILED(_terminal->Initialize());
*hwnd = _terminal->_hwnd.get();
*terminal = _terminal.release();
return S_OK;
}
void _stdcall TerminalRegisterScrollCallback(void* terminal, void __stdcall callback(int, int, int))
{
auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->RegisterScrollCallback(callback);
}
void _stdcall TerminalRegisterWriteCallback(void* terminal, const void __stdcall callback(wchar_t*))
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->RegisterWriteCallback(callback);
}
void _stdcall TerminalSendOutput(void* terminal, LPCWSTR data)
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->SendOutput(data);
}
HRESULT _stdcall TerminalTriggerResize(void* terminal, double width, double height, _Out_ COORD* dimensions)
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
const SIZE windowSize{ static_cast<short>(width), static_cast<short>(height) };
return publicTerminal->Refresh(windowSize, dimensions);
}
void _stdcall TerminalDpiChanged(void* terminal, int newDpi)
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
publicTerminal->_UpdateFont(newDpi);
}
void _stdcall TerminalUserScroll(void* terminal, int viewTop)
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
publicTerminal->_terminal->UserScrollViewport(viewTop);
}
HRESULT _stdcall TerminalStartSelection(void* terminal, COORD cursorPosition, bool altPressed)
{
COORD terminalPosition = { cursorPosition };
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
const auto fontSize = publicTerminal->_actualFont.GetSize();
RETURN_HR_IF(E_NOT_VALID_STATE, fontSize.X == 0);
RETURN_HR_IF(E_NOT_VALID_STATE, fontSize.Y == 0);
terminalPosition.X /= fontSize.X;
terminalPosition.Y /= fontSize.Y;
publicTerminal->_terminal->SetSelectionAnchor(terminalPosition);
publicTerminal->_terminal->SetBoxSelection(altPressed);
publicTerminal->_renderer->TriggerSelection();
return S_OK;
}
HRESULT _stdcall TerminalMoveSelection(void* terminal, COORD cursorPosition)
{
COORD terminalPosition = { cursorPosition };
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
const auto fontSize = publicTerminal->_actualFont.GetSize();
RETURN_HR_IF(E_NOT_VALID_STATE, fontSize.X == 0);
RETURN_HR_IF(E_NOT_VALID_STATE, fontSize.Y == 0);
terminalPosition.X /= fontSize.X;
terminalPosition.Y /= fontSize.Y;
publicTerminal->_terminal->SetEndSelectionPosition(terminalPosition);
publicTerminal->_renderer->TriggerSelection();
return S_OK;
}
void _stdcall TerminalClearSelection(void* terminal)
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
publicTerminal->_terminal->ClearSelection();
}
bool _stdcall TerminalIsSelectionActive(void* terminal)
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
const bool selectionActive = publicTerminal->_terminal->IsSelectionActive();
return selectionActive;
}
// Returns the selected text in the terminal.
const wchar_t* _stdcall TerminalGetSelection(void* terminal)
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
const auto bufferData = publicTerminal->_terminal->RetrieveSelectedTextFromBuffer(false);
// convert text: vector<string> --> string
std::wstring selectedText;
for (const auto& text : bufferData.text)
{
selectedText += text;
}
auto returnText = wil::make_cotaskmem_string_nothrow(selectedText.c_str());
TerminalClearSelection(terminal);
return returnText.release();
}
void _stdcall TerminalSendKeyEvent(void* terminal, WPARAM wParam)
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
const auto scanCode = MapVirtualKeyW((UINT)wParam, MAPVK_VK_TO_VSC);
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;
}
}
publicTerminal->_terminal->SendKeyEvent((WORD)wParam, (WORD)scanCode, flags);
}
void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch)
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
publicTerminal->_terminal->SendCharEvent(ch);
}
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, short fontSize, int newDpi)
{
const auto publicTerminal = static_cast<HwndTerminal*>(terminal);
{
auto lock = publicTerminal->_terminal->LockForWriting();
publicTerminal->_terminal->SetDefaultForeground(theme.DefaultForeground);
publicTerminal->_terminal->SetDefaultBackground(theme.DefaultBackground);
// 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)]] publicTerminal->_terminal->SetColorTableEntry(tableIndex, gsl::at(theme.ColorTable, tableIndex));
}
}
publicTerminal->_terminal->SetCursorStyle(theme.CursorStyle);
publicTerminal->_desiredFont = { fontFamily, 0, 10, { 0, fontSize }, CP_UTF8 };
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);
COORD dimensions = {};
const SIZE windowSize{ windowRect.right - windowRect.left, windowRect.bottom - windowRect.top };
publicTerminal->Refresh(windowSize, &dimensions);
}
// Resizes the terminal to the specified rows and columns.
HRESULT _stdcall TerminalResize(void* terminal, COORD dimensions)
{
const auto publicTerminal = static_cast<const HwndTerminal*>(terminal);
return publicTerminal->_terminal->UserResize(dimensions);
}

View File

@ -0,0 +1,73 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include "../../renderer/base/Renderer.hpp"
#include "../../renderer/dx/DxRenderer.hpp"
#include "../../cascadia/TerminalCore/Terminal.hpp"
using namespace Microsoft::Console::VirtualTerminal;
typedef struct _TerminalTheme
{
COLORREF DefaultBackground;
COLORREF DefaultForeground;
DispatchTypes::CursorStyle CursorStyle;
COLORREF ColorTable[16];
} TerminalTheme, *LPTerminalTheme;
extern "C" {
__declspec(dllexport) HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal);
__declspec(dllexport) void _stdcall TerminalSendOutput(void* terminal, LPCWSTR data);
__declspec(dllexport) void _stdcall TerminalRegisterScrollCallback(void* terminal, void __stdcall callback(int, int, int));
__declspec(dllexport) HRESULT _stdcall TerminalTriggerResize(void* terminal, double width, double height, _Out_ COORD* dimensions);
__declspec(dllexport) HRESULT _stdcall TerminalResize(void* terminal, COORD dimensions);
__declspec(dllexport) void _stdcall TerminalDpiChanged(void* terminal, int newDpi);
__declspec(dllexport) void _stdcall TerminalUserScroll(void* terminal, int viewTop);
__declspec(dllexport) HRESULT _stdcall TerminalStartSelection(void* terminal, COORD cursorPosition, bool altPressed);
__declspec(dllexport) HRESULT _stdcall TerminalMoveSelection(void* terminal, COORD cursorPosition);
__declspec(dllexport) void _stdcall TerminalClearSelection(void* terminal);
__declspec(dllexport) const wchar_t* _stdcall TerminalGetSelection(void* terminal);
__declspec(dllexport) bool _stdcall TerminalIsSelectionActive(void* terminal);
__declspec(dllexport) void _stdcall DestroyTerminal(void* terminal);
__declspec(dllexport) void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi);
__declspec(dllexport) void _stdcall TerminalRegisterWriteCallback(void* terminal, const void __stdcall callback(wchar_t*));
__declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WPARAM wParam);
__declspec(dllexport) void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch);
};
struct HwndTerminal
{
public:
HwndTerminal(HWND hwnd);
HRESULT Initialize();
void SendOutput(std::wstring_view data);
HRESULT Refresh(const SIZE windowSize, _Out_ COORD* dimensions);
void RegisterScrollCallback(std::function<void(int, int, int)> callback);
void RegisterWriteCallback(const void _stdcall callback(wchar_t*));
private:
wil::unique_hwnd _hwnd;
FontInfoDesired _desiredFont;
FontInfo _actualFont;
std::unique_ptr<::Microsoft::Terminal::Core::Terminal> _terminal;
std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer;
std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine;
friend HRESULT _stdcall CreateTerminal(HWND parentHwnd, _Out_ void** hwnd, _Out_ void** terminal);
friend HRESULT _stdcall TerminalResize(void* terminal, COORD dimensions);
friend void _stdcall TerminalDpiChanged(void* terminal, int newDpi);
friend void _stdcall TerminalUserScroll(void* terminal, int viewTop);
friend HRESULT _stdcall TerminalStartSelection(void* terminal, COORD cursorPosition, bool altPressed);
friend HRESULT _stdcall TerminalMoveSelection(void* terminal, COORD cursorPosition);
friend void _stdcall TerminalClearSelection(void* terminal);
friend const wchar_t* _stdcall TerminalGetSelection(void* terminal);
friend bool _stdcall TerminalIsSelectionActive(void* terminal);
friend void _stdcall TerminalSendKeyEvent(void* terminal, WPARAM wParam);
friend void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch);
friend void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, short fontSize, int newDpi);
void _UpdateFont(int newDpi);
};

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(SolutionDir)src\common.build.pre.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
</PropertyGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>16.0</VCProjectVersion>
<ProjectGuid>{84848BFA-931D-42CE-9ADF-01EE54DE7890}</ProjectGuid>
<Keyword>Win32Proj</Keyword>
<RootNamespace>PublicTerminalCore</RootNamespace>
<ProjectName>PublicTerminalCore</ProjectName>
</PropertyGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="HwndTerminal.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="HwndTerminal.hpp" />
<ClInclude Include="pch.h" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\terminal\input\lib\terminalinput.vcxproj">
<Project>{1cf55140-ef6a-4736-a403-957e4f7430bb}</Project>
</ProjectReference>
<ProjectReference Include="..\TerminalCore\lib\TerminalCore-lib.vcxproj">
<Project>{ca5cad1a-abcd-429c-b551-8562ec954746}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)src\types\lib\types.vcxproj">
<Project>{18D09A24-8240-42D6-8CB6-236EEE820263}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)src\buffer\out\lib\bufferout.vcxproj">
<Project>{0cf235bd-2da0-407e-90ee-c467e8bbc714}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)src\renderer\base\lib\base.vcxproj">
<Project>{af0a096a-8b3a-4949-81ef-7df8f0fee91f}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)src\terminal\parser\lib\parser.vcxproj">
<Project>{3ae13314-1939-4dfa-9c14-38ca0834050c}</Project>
</ProjectReference>
<ProjectReference Include="$(OpenConsoleDir)src\renderer\dx\lib\dx.vcxproj">
<Project>{48d21369-3d7b-4431-9967-24e81292cf62}</Project>
</ProjectReference>
</ItemGroup>
<ItemDefinitionGroup>
<ClCompile>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
</ClCompile>
</ItemDefinitionGroup>
<Import Project="$(SolutionDir)src\common.build.dll.props" />
<Import Project="$(SolutionDir)src\common.build.post.props" />
</Project>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<Natvis Include="$(SolutionDir)tools\ConsoleTypes.natvis" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HwndTerminal.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HwndTerminal.hpp">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"

View File

@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include <LibraryIncludes.h>

View File

@ -42,7 +42,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_desiredFont{ DEFAULT_FONT_FACE.c_str(), 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8 },
_actualFont{ DEFAULT_FONT_FACE.c_str(), 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false },
_touchAnchor{ std::nullopt },
_leadingSurrogate{},
_cursorTimer{},
_lastMouseClick{},
_lastMouseClickPos{}
@ -578,44 +577,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
const auto ch = e.Character();
// We want Backspace to be handled by _KeyDownHandler, so the
// terminalInput can translate it into a \x7f. So, do nothing here, so
// we don't end up sending both a BS and a DEL to the terminal.
// Also skip processing DEL here, which will hit here when Ctrl+Bkspc is
// pressed, but after it's handled by the _KeyDownHandler below.
if (ch == UNICODE_BACKSPACE || ch == UNICODE_DEL)
{
return;
}
else if (Utf16Parser::IsLeadingSurrogate(ch))
{
if (_leadingSurrogate.has_value())
{
// we already were storing a leading surrogate but we got another one. Go ahead and send the
// saved surrogate piece and save the new one
auto hstr = to_hstring(_leadingSurrogate.value());
_connection.WriteInput(hstr);
}
// save the leading portion of a surrogate pair so that they can be sent at the same time
_leadingSurrogate.emplace(ch);
}
else if (_leadingSurrogate.has_value())
{
std::wstring wstr;
wstr.reserve(2);
wstr.push_back(_leadingSurrogate.value());
wstr.push_back(ch);
_leadingSurrogate.reset();
auto hstr = to_hstring(wstr.c_str());
_connection.WriteInput(hstr);
}
else
{
auto hstr = to_hstring(ch);
_connection.WriteInput(hstr);
}
e.Handled(true);
const bool handled = _terminal->SendCharEvent(ch);
e.Handled(handled);
}
void TermControl::_KeyDownHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/,

View File

@ -10,6 +10,11 @@ namespace Microsoft::Terminal::Core
{
public:
virtual ~ITerminalApi() {}
ITerminalApi(const ITerminalApi&) = default;
ITerminalApi(ITerminalApi&&) = default;
ITerminalApi& operator=(const ITerminalApi&) = default;
ITerminalApi& operator=(ITerminalApi&&) = default;
virtual bool PrintString(std::wstring_view stringView) = 0;
virtual bool ExecuteChar(wchar_t wch) = 0;
@ -38,5 +43,8 @@ namespace Microsoft::Terminal::Core
virtual bool SetDefaultForeground(const DWORD dwColor) = 0;
virtual bool SetDefaultBackground(const DWORD dwColor) = 0;
protected:
ITerminalApi() = default;
};
}

View File

@ -11,12 +11,20 @@ namespace Microsoft::Terminal::Core
{
public:
virtual ~ITerminalInput() {}
ITerminalInput(const ITerminalInput&) = default;
ITerminalInput(ITerminalInput&&) = default;
ITerminalInput& operator=(const ITerminalInput&) = default;
ITerminalInput& operator=(ITerminalInput&&) = default;
virtual bool SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates states) = 0;
virtual bool SendCharEvent(const wchar_t ch) = 0;
// void SendMouseEvent(uint row, uint col, KeyModifiers modifiers);
[[nodiscard]] virtual HRESULT UserResize(const COORD size) noexcept = 0;
virtual void UserScrollViewport(const int viewTop) = 0;
virtual int GetScrollOffset() = 0;
protected:
ITerminalInput() = default;
};
}

View File

@ -259,6 +259,11 @@ bool Terminal::SendKeyEvent(const WORD vkey, const WORD scanCode, const ControlK
return translated && manuallyHandled;
}
bool Terminal::SendCharEvent(const wchar_t ch)
{
return _terminalInput->HandleChar(ch);
}
// Method Description:
// - Returns the keyboard's scan code for the given virtual key code.
// Arguments:

View File

@ -37,7 +37,11 @@ class Microsoft::Terminal::Core::Terminal final :
{
public:
Terminal();
virtual ~Terminal(){};
~Terminal(){};
Terminal(const Terminal&) = default;
Terminal(Terminal&&) = default;
Terminal& operator=(const Terminal&) = default;
Terminal& operator=(Terminal&&) = default;
void Create(COORD viewportSize,
SHORT scrollbackLines,
@ -84,6 +88,8 @@ public:
#pragma region ITerminalInput
// These methods are defined in Terminal.cpp
bool SendKeyEvent(const WORD vkey, const WORD scanCode, const Microsoft::Terminal::Core::ControlKeyStates states) override;
bool SendCharEvent(const wchar_t ch) override;
[[nodiscard]] HRESULT UserResize(const COORD viewportSize) noexcept override;
void UserScrollViewport(const int viewTop) override;
int GetScrollOffset() override;

View File

@ -0,0 +1,43 @@
// <copyright file="ITerminalConnection.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// </copyright>
namespace Microsoft.Terminal.Wpf
{
using System;
/// <summary>
/// Represents a connection to a terminal backend, generally a pty interface.
/// </summary>
public interface ITerminalConnection
{
/// <summary>
/// Event that should be triggered when the terminal backend has new data for the terminal control.
/// </summary>
event EventHandler<TerminalOutputEventArgs> TerminalOutput;
/// <summary>
/// Inform the backend that the terminal control is ready to start sending and receiving data.
/// </summary>
void Start();
/// <summary>
/// Write user input to the backend.
/// </summary>
/// <param name="data">The data to be written to the terminal backend.</param>
void WriteInput(string data);
/// <summary>
/// Resize the terminal backend.
/// </summary>
/// <param name="rows">The number of rows to resize to.</param>
/// <param name="columns">The number of columns to resize to.</param>
void Resize(uint rows, uint columns);
/// <summary>
/// Shut down the terminal backend process.
/// </summary>
void Close();
}
}

View File

@ -0,0 +1,238 @@
// <copyright file="NativeMethods.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// </copyright>
namespace Microsoft.Terminal.Wpf
{
using System;
using System.Runtime.InteropServices;
#pragma warning disable SA1600 // Elements should be documented
internal static class NativeMethods
{
public const int USER_DEFAULT_SCREEN_DPI = 96;
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void ScrollCallback(int viewTop, int viewHeight, int bufferSize);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate void WriteCallback([In, MarshalAs(UnmanagedType.LPWStr)] string data);
public enum WindowMessage : int
{
/// <summary>
/// The WM_MOUSEACTIVATE message is sent when the cursor is in an inactive window and the user presses a mouse button. The parent window receives this message only if the child window passes it to the DefWindowProc function.
/// </summary>
WM_MOUSEACTIVATE = 0x0021,
/// <summary>
/// The WM_WINDOWPOSCHANGED message is sent to a window whose size, position, or place in the Z order has changed as a result of a call to the SetWindowPos function or another window-management function.
/// </summary>
WM_WINDOWPOSCHANGED = 0x0047,
/// <summary>
/// The WM_KEYDOWN message is posted to the window with the keyboard focus when a nonsystem key is pressed. A nonsystem key is a key that is pressed when the ALT key is not pressed.
/// </summary>
WM_KEYDOWN = 0x0100,
/// <summary>
/// The WM_CHAR message is posted to the window with the keyboard focus when a WM_KEYDOWN message is translated by the TranslateMessage function. The WM_CHAR message contains the character code of the key that was pressed.
/// </summary>
WM_CHAR = 0x0102,
/// <summary>
/// The WM_MOUSEMOVE message is posted to a window when the cursor moves. If the mouse is not captured, the message is posted to the window that contains the cursor. Otherwise, the message is posted to the window that has captured the mouse.
/// </summary>
WM_MOUSEMOVE = 0x200,
/// <summary>
/// The WM_LBUTTONDOWN message is posted when the user presses the left mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse.
/// </summary>
WM_LBUTTONDOWN = 0x0201,
/// <summary>
/// The WM_RBUTTONDOWN message is posted when the user presses the right mouse button while the cursor is in the client area of a window. If the mouse is not captured, the message is posted to the window beneath the cursor. Otherwise, the message is posted to the window that has captured the mouse.
/// </summary>
WM_RBUTTONDOWN = 0x0204,
/// <summary>
/// The WM_MOUSEWHEEL message is sent to the focus window when the mouse wheel is rotated. The DefWindowProc function propagates the message to the window's parent. There should be no internal forwarding of the message, since DefWindowProc propagates it up the parent chain until it finds a window that processes it.
/// </summary>
WM_MOUSEWHEEL = 0x020A,
}
public enum VirtualKey : ushort
{
/// <summary>
/// ALT key
/// </summary>
VK_MENU = 0x12,
}
[Flags]
public enum SetWindowPosFlags : uint
{
/// <summary>
/// If the calling thread and the thread that owns the window are attached to different input queues, the system posts the request to the thread that owns the window. This prevents the calling thread from blocking its execution while other threads process the request.
/// </summary>
SWP_ASYNCWINDOWPOS = 0x4000,
/// <summary>
/// Prevents generation of the WM_SYNCPAINT message.
/// </summary>
SWP_DEFERERASE = 0x2000,
/// <summary>
/// Draws a frame (defined in the window's class description) around the window.
/// </summary>
SWP_DRAWFRAME = 0x0020,
/// <summary>
/// Applies new frame styles set using the SetWindowLong function. Sends a WM_NCCALCSIZE message to the window, even if the window's size is not being changed. If this flag is not specified, WM_NCCALCSIZE is sent only when the window's size is being changed.
/// </summary>
SWP_FRAMECHANGED = 0x0020,
/// <summary>
/// Hides the window.
/// </summary>
SWP_HIDEWINDOW = 0x0080,
/// <summary>
/// Does not activate the window. If this flag is not set, the window is activated and moved to the top of either the topmost or non-topmost group (depending on the setting of the hWndInsertAfter parameter).
/// </summary>
SWP_NOACTIVATE = 0x0010,
/// <summary>
/// Discards the entire contents of the client area. If this flag is not specified, the valid contents of the client area are saved and copied back into the client area after the window is sized or repositioned.
/// </summary>
SWP_NOCOPYBITS = 0x0100,
/// <summary>
/// Retains the current position (ignores X and Y parameters).
/// </summary>
SWP_NOMOVE = 0x0002,
/// <summary>
/// Does not change the owner window's position in the Z order.
/// </summary>
SWP_NOOWNERZORDER = 0x0200,
/// <summary>
/// Does not redraw changes. If this flag is set, no repainting of any kind occurs. This applies to the client area, the nonclient area (including the title bar and scroll bars), and any part of the parent window uncovered as a result of the window being moved. When this flag is set, the application must explicitly invalidate or redraw any parts of the window and parent window that need redrawing.
/// </summary>
SWP_NOREDRAW = 0x0008,
/// <summary>
/// Same as the SWP_NOOWNERZORDER flag.
/// </summary>
SWP_NOREPOSITION = 0x0200,
/// <summary>
/// Prevents the window from receiving the WM_WINDOWPOSCHANGING message.
/// </summary>
SWP_NOSENDCHANGING = 0x0400,
/// <summary>
/// Retains the current size (ignores the cx and cy parameters).
/// </summary>
SWP_NOSIZE = 0x0001,
/// <summary>
/// Retains the current Z order (ignores the hWndInsertAfter parameter).
/// </summary>
SWP_NOZORDER = 0x0004,
/// <summary>
/// Displays the window.
/// </summary>
SWP_SHOWWINDOW = 0x0040,
}
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern uint CreateTerminal(IntPtr parent, out IntPtr hwnd, out IntPtr terminal);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void TerminalSendOutput(IntPtr terminal, string lpdata);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern uint TerminalTriggerResize(IntPtr terminal, double width, double height, out COORD dimensions);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern uint TerminalResize(IntPtr terminal, COORD dimensions);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void TerminalDpiChanged(IntPtr terminal, int newDpi);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void TerminalRegisterScrollCallback(IntPtr terminal, [MarshalAs(UnmanagedType.FunctionPtr)]ScrollCallback callback);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void TerminalRegisterWriteCallback(IntPtr terminal, [MarshalAs(UnmanagedType.FunctionPtr)]WriteCallback callback);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void TerminalUserScroll(IntPtr terminal, int viewTop);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern uint TerminalStartSelection(IntPtr terminal, NativeMethods.COORD cursorPosition, bool altPressed);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern uint TerminalMoveSelection(IntPtr terminal, NativeMethods.COORD cursorPosition);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void TerminalClearSelection(IntPtr terminal);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.LPWStr)]
public static extern string TerminalGetSelection(IntPtr terminal);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
[return: MarshalAs(UnmanagedType.I1)]
public static extern bool TerminalIsSelectionActive(IntPtr terminal);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void DestroyTerminal(IntPtr terminal);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void TerminalSendKeyEvent(IntPtr terminal, IntPtr wParam);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void TerminalSendCharEvent(IntPtr terminal, char ch);
[DllImport("PublicTerminalCore.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern void TerminalSetTheme(IntPtr terminal, [MarshalAs(UnmanagedType.Struct)] TerminalTheme theme, string fontFamily, short fontSize, int newDpi);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetFocus(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true)]
public static extern short GetKeyState(int keyCode);
[StructLayout(LayoutKind.Sequential)]
public struct WINDOWPOS
{
public IntPtr hwnd;
public IntPtr hwndInsertAfter;
public int x;
public int y;
public int cx;
public int cy;
public uint flags;
}
[StructLayout(LayoutKind.Sequential)]
public struct COORD
{
/// <summary>
/// The x-coordinate of the point.
/// </summary>
public short X;
/// <summary>
/// The x-coordinate of the point.
/// </summary>
public short Y;
}
}
#pragma warning restore SA1600 // Elements should be documented
}

View File

@ -0,0 +1,289 @@
// <copyright file="TerminalContainer.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// </copyright>
namespace Microsoft.Terminal.Wpf
{
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
/// <summary>
/// The container class that hosts the native hwnd terminal.
/// </summary>
/// <remarks>
/// This class is only left public since xaml cannot work with internal classes.
/// </remarks>
public class TerminalContainer : HwndHost
{
private ITerminalConnection connection;
private IntPtr hwnd;
private IntPtr terminal;
private NativeMethods.ScrollCallback scrollCallback;
private NativeMethods.WriteCallback writeCallback;
// We keep track of the DPI scale since we could get a DPI changed event before we are able to initialize the terminal.
private DpiScale dpiScale = new DpiScale(NativeMethods.USER_DEFAULT_SCREEN_DPI, NativeMethods.USER_DEFAULT_SCREEN_DPI);
/// <summary>
/// Initializes a new instance of the <see cref="TerminalContainer"/> class.
/// </summary>
public TerminalContainer()
{
this.MessageHook += this.TerminalContainer_MessageHook;
this.GotFocus += this.TerminalContainer_GotFocus;
this.Focusable = true;
}
/// <summary>
/// Event that is fired when the terminal buffer scrolls from text output.
/// </summary>
internal event EventHandler<(int viewTop, int viewHeight, int bufferSize)> TerminalScrolled;
/// <summary>
/// Event that is fired when the user engages in a mouse scroll over the terminal hwnd.
/// </summary>
internal event EventHandler<int> UserScrolled;
/// <summary>
/// Gets the character rows available to the terminal.
/// </summary>
internal int Rows { get; private set; }
/// <summary>
/// Gets the character columns available to the terminal.
/// </summary>
internal int Columns { get; private set; }
/// <summary>
/// Sets the connection to the terminal backend.
/// </summary>
internal ITerminalConnection Connection
{
private get
{
return this.connection;
}
set
{
if (this.connection != null)
{
this.connection.TerminalOutput -= this.Connection_TerminalOutput;
}
this.connection = value;
this.connection.TerminalOutput += this.Connection_TerminalOutput;
this.connection.Start();
}
}
/// <summary>
/// Manually invoke a scroll of the terminal buffer.
/// </summary>
/// <param name="viewTop">The top line to show in the terminal.</param>
internal void UserScroll(int viewTop)
{
NativeMethods.TerminalUserScroll(this.terminal, viewTop);
}
/// <summary>
/// Sets the theme for the terminal. This includes font family, size, color, as well as background and foreground colors.
/// </summary>
/// <param name="theme">The color theme for the terminal to use.</param>
/// <param name="fontFamily">The font family to use in the terminal.</param>
/// <param name="fontSize">The font size to use in the terminal.</param>
/// <param name="newDpi">The dpi that the terminal should be rendered at.</param>
internal void SetTheme(TerminalTheme theme, string fontFamily, short fontSize, int newDpi)
{
NativeMethods.TerminalSetTheme(this.terminal, theme, fontFamily, fontSize, newDpi);
}
/// <summary>
/// Triggers a refresh of the terminal with the given size.
/// </summary>
/// <param name="renderSize">Size of the rendering window.</param>
/// <returns>Tuple with rows and columns.</returns>
internal (int rows, int columns) TriggerResize(Size renderSize)
{
NativeMethods.COORD dimensions;
NativeMethods.TerminalTriggerResize(this.terminal, renderSize.Width, renderSize.Height, out dimensions);
this.Rows = dimensions.Y;
this.Columns = dimensions.X;
return (dimensions.Y, dimensions.X);
}
/// <summary>
/// Resizes the terminal.
/// </summary>
/// <param name="rows">Number of rows to show.</param>
/// <param name="columns">Number of columns to show.</param>
internal void Resize(uint rows, uint columns)
{
NativeMethods.COORD dimensions = new NativeMethods.COORD
{
X = (short)columns,
Y = (short)rows,
};
NativeMethods.TerminalResize(this.terminal, dimensions);
}
/// <inheritdoc/>
protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi)
{
// Save the DPI if the terminal hasn't been initialized.
if (this.terminal == IntPtr.Zero)
{
this.dpiScale = newDpi;
}
else
{
NativeMethods.TerminalDpiChanged(this.terminal, (int)(NativeMethods.USER_DEFAULT_SCREEN_DPI * newDpi.DpiScaleX));
}
}
/// <inheritdoc/>
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
NativeMethods.CreateTerminal(hwndParent.Handle, out this.hwnd, out this.terminal);
this.scrollCallback = this.OnScroll;
this.writeCallback = this.OnWrite;
NativeMethods.TerminalRegisterScrollCallback(this.terminal, this.scrollCallback);
NativeMethods.TerminalRegisterWriteCallback(this.terminal, this.writeCallback);
// If the saved DPI scale isn't the default scale, we push it to the terminal.
if (this.dpiScale.DpiScaleX != NativeMethods.USER_DEFAULT_SCREEN_DPI)
{
NativeMethods.TerminalDpiChanged(this.terminal, (int)(NativeMethods.USER_DEFAULT_SCREEN_DPI * this.dpiScale.DpiScaleX));
}
return new HandleRef(this, this.hwnd);
}
/// <inheritdoc/>
protected override void DestroyWindowCore(HandleRef hwnd)
{
NativeMethods.DestroyTerminal(this.terminal);
this.terminal = IntPtr.Zero;
}
private void TerminalContainer_GotFocus(object sender, RoutedEventArgs e)
{
e.Handled = true;
NativeMethods.SetFocus(this.hwnd);
}
private IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (hwnd == this.hwnd)
{
switch ((NativeMethods.WindowMessage)msg)
{
case NativeMethods.WindowMessage.WM_MOUSEACTIVATE:
this.Focus();
NativeMethods.SetFocus(this.hwnd);
break;
case NativeMethods.WindowMessage.WM_LBUTTONDOWN:
this.LeftClickHandler((int)lParam);
break;
case NativeMethods.WindowMessage.WM_RBUTTONDOWN:
if (NativeMethods.TerminalIsSelectionActive(this.terminal))
{
Clipboard.SetText(NativeMethods.TerminalGetSelection(this.terminal));
}
else
{
this.connection.WriteInput(Clipboard.GetText());
}
break;
case NativeMethods.WindowMessage.WM_MOUSEMOVE:
this.MouseMoveHandler((int)wParam, (int)lParam);
break;
case NativeMethods.WindowMessage.WM_KEYDOWN:
NativeMethods.TerminalClearSelection(this.terminal);
NativeMethods.TerminalSendKeyEvent(this.terminal, wParam);
break;
case NativeMethods.WindowMessage.WM_CHAR:
NativeMethods.TerminalSendCharEvent(this.terminal, (char)wParam);
break;
case NativeMethods.WindowMessage.WM_WINDOWPOSCHANGED:
var windowpos = (NativeMethods.WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(NativeMethods.WINDOWPOS));
if (((NativeMethods.SetWindowPosFlags)windowpos.flags).HasFlag(NativeMethods.SetWindowPosFlags.SWP_NOSIZE))
{
break;
}
NativeMethods.TerminalTriggerResize(this.terminal, windowpos.cx, windowpos.cy, out var dimensions);
this.connection?.Resize((uint)dimensions.Y, (uint)dimensions.X);
this.Columns = dimensions.X;
this.Rows = dimensions.Y;
break;
case NativeMethods.WindowMessage.WM_MOUSEWHEEL:
var delta = ((int)wParam) >> 16;
this.UserScrolled?.Invoke(this, delta);
break;
}
}
return IntPtr.Zero;
}
private void LeftClickHandler(int lParam)
{
var altPressed = NativeMethods.GetKeyState((int)NativeMethods.VirtualKey.VK_MENU) < 0;
var x = (short)(((int)lParam << 16) >> 16);
var y = (short)((int)lParam >> 16);
NativeMethods.COORD cursorPosition = new NativeMethods.COORD()
{
X = x,
Y = y,
};
NativeMethods.TerminalStartSelection(this.terminal, cursorPosition, altPressed);
}
private void MouseMoveHandler(int wParam, int lParam)
{
if (((int)wParam & 0x0001) == 1)
{
var x = (short)(((int)lParam << 16) >> 16);
var y = (short)((int)lParam >> 16);
NativeMethods.COORD cursorPosition = new NativeMethods.COORD()
{
X = x,
Y = y,
};
NativeMethods.TerminalMoveSelection(this.terminal, cursorPosition);
}
}
private void Connection_TerminalOutput(object sender, TerminalOutputEventArgs e)
{
if (this.terminal != IntPtr.Zero)
{
NativeMethods.TerminalSendOutput(this.terminal, e.Data);
}
}
private void OnScroll(int viewTop, int viewHeight, int bufferSize)
{
this.TerminalScrolled?.Invoke(this, (viewTop, viewHeight, bufferSize));
}
private void OnWrite(string data)
{
this.connection?.WriteInput(data);
}
}
}

View File

@ -0,0 +1,18 @@
<UserControl x:Class="Microsoft.Terminal.Wpf.TerminalControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Microsoft.Terminal.Wpf"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Focusable="True">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<local:TerminalContainer x:Name="termContainer"/>
<ScrollBar x:Name="scrollbar" Scroll="Scrollbar_Scroll" Grid.Column="1" />
</Grid>
</UserControl>

View File

@ -0,0 +1,144 @@
// <copyright file="TerminalControl.xaml.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// </copyright>
namespace Microsoft.Terminal.Wpf
{
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
/// <summary>
/// A basic terminal control. This control can receive and render standard VT100 sequences.
/// </summary>
public partial class TerminalControl : UserControl
{
private int accumulatedDelta = 0;
/// <summary>
/// Initializes a new instance of the <see cref="TerminalControl"/> class.
/// </summary>
public TerminalControl()
{
this.InitializeComponent();
this.termContainer.TerminalScrolled += this.TermControl_TerminalScrolled;
this.termContainer.UserScrolled += this.TermControl_UserScrolled;
this.scrollbar.MouseWheel += this.Scrollbar_MouseWheel;
this.GotFocus += this.TerminalControl_GotFocus;
}
/// <summary>
/// Gets the character rows available to the terminal.
/// </summary>
public int Rows => this.termContainer.Rows;
/// <summary>
/// Gets the character columns available to the terminal.
/// </summary>
public int Columns => this.termContainer.Columns;
/// <summary>
/// Sets the connection to a terminal backend.
/// </summary>
public ITerminalConnection Connection
{
set
{
this.termContainer.Connection = value;
}
}
/// <summary>
/// Sets the theme for the terminal. This includes font family, size, color, as well as background and foreground colors.
/// </summary>
/// <param name="theme">The color theme to use in the terminal.</param>
/// <param name="fontFamily">The font family to use in the terminal.</param>
/// <param name="fontSize">The font size to use in the terminal.</param>
public void SetTheme(TerminalTheme theme, string fontFamily, short fontSize)
{
PresentationSource source = PresentationSource.FromVisual(this);
if (source == null)
{
return;
}
int dpiX;
dpiX = (int)(NativeMethods.USER_DEFAULT_SCREEN_DPI * source.CompositionTarget.TransformToDevice.M11);
this.termContainer.SetTheme(theme, fontFamily, fontSize, dpiX);
}
/// <summary>
/// Resizes the terminal to the specified rows and columns.
/// </summary>
/// <param name="rows">Number of rows to display.</param>
/// <param name="columns">Number of columns to display.</param>
public void Resize(uint rows, uint columns)
{
this.termContainer.Resize(rows, columns);
}
/// <summary>
/// Resizes the terminal to the specified dimensions.
/// </summary>
/// <param name="rendersize">Rendering size for the terminal.</param>
/// <returns>A tuple of (int, int) representing the number of rows and columns in the terminal.</returns>
public (int rows, int columns) TriggerResize(Size rendersize)
{
return this.termContainer.TriggerResize(rendersize);
}
private void TerminalControl_GotFocus(object sender, RoutedEventArgs e)
{
e.Handled = true;
this.termContainer.Focus();
}
private void Scrollbar_MouseWheel(object sender, MouseWheelEventArgs e)
{
this.TermControl_UserScrolled(sender, e.Delta);
}
private void TermControl_UserScrolled(object sender, int delta)
{
var lineDelta = 120 / SystemParameters.WheelScrollLines;
this.accumulatedDelta += delta;
if (this.accumulatedDelta < lineDelta && this.accumulatedDelta > -lineDelta)
{
return;
}
this.Dispatcher.InvokeAsync(() =>
{
var lines = -this.accumulatedDelta / lineDelta;
this.scrollbar.Value += lines;
this.accumulatedDelta = 0;
this.termContainer.UserScroll((int)this.scrollbar.Value);
});
}
private void TermControl_TerminalScrolled(object sender, (int viewTop, int viewHeight, int bufferSize) e)
{
this.Dispatcher.InvokeAsync(() =>
{
this.scrollbar.Minimum = 0;
this.scrollbar.Maximum = e.bufferSize - e.viewHeight;
this.scrollbar.Value = e.viewTop;
this.scrollbar.ViewportSize = e.viewHeight;
});
}
private void Scrollbar_Scroll(object sender, System.Windows.Controls.Primitives.ScrollEventArgs e)
{
var viewTop = (int)e.NewValue;
this.termContainer.UserScroll(viewTop);
}
}
}

View File

@ -0,0 +1,29 @@
// <copyright file="TerminalOutputEventArgs.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// </copyright>
namespace Microsoft.Terminal.Wpf
{
using System;
/// <summary>
/// Event args for output from the terminal backend.
/// </summary>
public class TerminalOutputEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="TerminalOutputEventArgs"/> class.
/// </summary>
/// <param name="data">The backend data associated with the event.</param>
public TerminalOutputEventArgs(string data)
{
this.Data = data;
}
/// <summary>
/// Gets the data sent from the terminal backend.
/// </summary>
public string Data { get; }
}
}

View File

@ -0,0 +1,80 @@
// <copyright file="TerminalTheme.cs" company="Microsoft Corporation">
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
// </copyright>
namespace Microsoft.Terminal.Wpf
{
using System;
using System.Runtime.InteropServices;
/// <summary>
/// Enum for the style of cursor to display in the terminal.
/// </summary>
public enum CursorStyle : uint
{
/// <summary>
/// Cursor will be rendered as a blinking block.
/// </summary>
BlinkingBlock = 0,
/// <summary>
/// Currently identical to <see cref="CursorStyle.BlinkingBlock"/>
/// </summary>
BlinkingBlockDefault = 1,
/// <summary>
/// Cursor will be rendered as a block that does not blink.
/// </summary>
SteadyBlock = 2,
/// <summary>
/// Cursor will be rendered as a blinking underline.
/// </summary>
BlinkingUnderline = 3,
/// <summary>
/// Cursor will be rendered as an underline that does not blink.
/// </summary>
SteadyUnderline = 4,
/// <summary>
/// Cursor will be rendered as a vertical blinking bar.
/// </summary>
BlinkingBar = 5,
/// <summary>
/// Cursor will be rendered as a vertical bar that does not blink.
/// </summary>
SteadyBar = 6,
}
/// <summary>
/// Structure for color handling in the terminal.
/// </summary>
/// <remarks>Pack = 1 removes the padding added by some compilers/processors for optimization purposes.</remarks>
[StructLayout(LayoutKind.Sequential)]
public struct TerminalTheme
{
/// <summary>
/// The default background color of the terminal, represented in Win32 COLORREF format.
/// </summary>
public uint DefaultBackground;
/// <summary>
/// The default foreground color of the terminal, represented in Win32 COLORREF format.
/// </summary>
public uint DefaultForeground;
/// <summary>
/// The style of cursor to use in the terminal.
/// </summary>
public CursorStyle CursorStyle;
/// <summary>
/// The color array to use for the terminal, filling the standard vt100 16 color table, represented in Win32 COLORREF format.
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.U4, SizeConst = 16)]
public uint[] ColorTable;
}
}

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.props" />
<PropertyGroup>
<TargetFrameworks>net472</TargetFrameworks>
<RootNamespace>Microsoft.Terminal.Wpf</RootNamespace>
<AssemblyName>Microsoft.Terminal.Wpf</AssemblyName>
<RepoBinPath>$(MSBuildThisFileDirectory)..\..\..\bin\</RepoBinPath>
<IntermediateOutputPath>$(RepoBinPath)..\obj\$(Platform)\$(Configuration)\$(MSBuildProjectName)</IntermediateOutputPath>
<OutputPath>$(RepoBinPath)$(Platform)\$(Configuration)\$(MSBuildProjectName)</OutputPath>
<BeforePack>CollectNativePackContents</BeforePack>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>0.5</Version>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\PublicTerminalCore\PublicTerminalCore.vcxproj" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json">
<Visible>true</Visible>
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Page Include="**\*.xaml" Exclude="App.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Compile Update="**\*.xaml.cs">
<DependentUpon>%(Filename)</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<Target Name="CollectNativePackContents">
<ItemGroup>
<None Include="$(RepoBinPath)\Win32\$(Configuration)\PublicTerminalCore.dll">
<Pack>true</Pack>
<PackagePath>runtimes\win-x86\native\</PackagePath>
</None>
<None Include="$(RepoBinPath)\x64\$(Configuration)\PublicTerminalCore.dll">
<Pack>true</Pack>
<PackagePath>runtimes\win-x64\native\</PackagePath>
</None>
</ItemGroup>
</Target>
<ItemGroup>
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="System.Xaml" />
<Reference Include="WindowsBase" />
</ItemGroup>
<Import Sdk="Microsoft.NET.Sdk" Project="Sdk.targets" />
</Project>

View File

@ -0,0 +1,12 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"companyName": "Microsoft Corporation",
"copyrightText": "Copyright (c) {companyName}.\nLicensed under the {licenseName} license.",
"variables": {
"licenseName": "MIT"
}
}
}
}

View File

@ -925,16 +925,21 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
// - S_OK
[[nodiscard]] HRESULT DxEngine::PaintBackground() noexcept
{
/*_d2dRenderTarget->FillRectangle(D2D1::RectF((float)_invalidRect.left,
(float)_invalidRect.top,
(float)_invalidRect.right,
(float)_invalidRect.bottom),
_d2dBrushBackground.Get());
*/
switch (_chainMode)
{
case SwapChainMode::ForHwnd:
_d2dRenderTarget->FillRectangle(D2D1::RectF(static_cast<float>(_invalidRect.left),
static_cast<float>(_invalidRect.top),
static_cast<float>(_invalidRect.right),
static_cast<float>(_invalidRect.bottom)),
_d2dBrushBackground.Get());
break;
case SwapChainMode::ForComposition:
D2D1_COLOR_F nothing = { 0 };
D2D1_COLOR_F nothing = { 0 };
_d2dRenderTarget->Clear(nothing);
_d2dRenderTarget->Clear(nothing);
break;
}
return S_OK;
}

View File

@ -40,7 +40,12 @@ namespace Microsoft::Console::Render
class IRenderData : public Microsoft::Console::Types::IBaseData
{
public:
virtual ~IRenderData() = 0;
~IRenderData() = 0;
IRenderData(const IRenderData&) = default;
IRenderData(IRenderData&&) = default;
IRenderData& operator=(const IRenderData&) = default;
IRenderData& operator=(IRenderData&&) = default;
virtual const TextAttribute GetDefaultBrushColors() noexcept = 0;
virtual const COLORREF GetForegroundColor(const TextAttribute& attr) const noexcept = 0;
@ -59,6 +64,9 @@ namespace Microsoft::Console::Render
virtual const bool IsGridLineDrawingAllowed() noexcept = 0;
virtual const std::wstring GetConsoleTitle() const noexcept = 0;
protected:
IRenderData() = default;
};
// See docs/virtual-dtors.md for an explanation of why this is weird.

View File

@ -19,9 +19,17 @@ namespace Microsoft::Console::Render
{
public:
virtual ~IRenderThread() = 0;
IRenderThread(const IRenderThread&) = default;
IRenderThread(IRenderThread&&) = default;
IRenderThread& operator=(const IRenderThread&) = default;
IRenderThread& operator=(IRenderThread&&) = default;
virtual void NotifyPaint() = 0;
virtual void EnablePainting() = 0;
virtual void WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs) = 0;
protected:
IRenderThread() = default;
};
inline Microsoft::Console::Render::IRenderThread::~IRenderThread(){};

View File

@ -24,7 +24,11 @@ namespace Microsoft::Console::Render
class IRenderer : public IRenderTarget
{
public:
virtual ~IRenderer() = 0;
~IRenderer() = 0;
IRenderer(const IRenderer&) = default;
IRenderer(IRenderer&&) = default;
IRenderer& operator=(const IRenderer&) = default;
IRenderer& operator=(IRenderer&&) = default;
[[nodiscard]] virtual HRESULT PaintFrame() = 0;
@ -56,6 +60,9 @@ namespace Microsoft::Console::Render
virtual void WaitForPaintCompletionAndDisable(const DWORD dwTimeoutMs) = 0;
virtual void AddRenderEngine(_In_ IRenderEngine* const pEngine) = 0;
protected:
IRenderer() = default;
};
inline Microsoft::Console::Render::IRenderer::~IRenderer() {}

View File

@ -15,12 +15,14 @@
#endif
#include "..\..\inc\unicode.hpp"
#include "..\..\types\inc\Utf16Parser.hpp"
using namespace Microsoft::Console::VirtualTerminal;
DWORD const dwAltGrFlags = LEFT_CTRL_PRESSED | RIGHT_ALT_PRESSED;
TerminalInput::TerminalInput(_In_ std::function<void(std::deque<std::unique_ptr<IInputEvent>>&)> pfn)
TerminalInput::TerminalInput(_In_ std::function<void(std::deque<std::unique_ptr<IInputEvent>>&)> pfn) :
_leadingSurrogate{}
{
_pfnWriteEvents = pfn;
}
@ -466,6 +468,45 @@ bool TerminalInput::HandleKey(const IInputEvent* const pInEvent) const
return fKeyHandled;
}
bool TerminalInput::HandleChar(const wchar_t ch)
{
if (ch == UNICODE_BACKSPACE || ch == UNICODE_DEL)
{
return false;
}
else if (Utf16Parser::IsLeadingSurrogate(ch))
{
if (_leadingSurrogate.has_value())
{
// we already were storing a leading surrogate but we got another one. Go ahead and send the
// saved surrogate piece and save the new one
wchar_t buffer[32];
swprintf_s(buffer, L"%I32u", _leadingSurrogate.value());
_SendInputSequence(buffer);
}
// save the leading portion of a surrogate pair so that they can be sent at the same time
_leadingSurrogate.emplace(ch);
}
else if (_leadingSurrogate.has_value())
{
std::wstring wstr;
wstr.reserve(2);
wstr.push_back(_leadingSurrogate.value());
wstr.push_back(ch);
_leadingSurrogate.reset();
_SendInputSequence(wstr);
}
else
{
wchar_t buffer[2] = { ch, 0 };
std::wstring_view buffer_view(buffer, 1);
_SendInputSequence(buffer_view);
}
return true;
}
// Routine Description:
// - Sends the given char as a sequence representing Alt+wch, also the same as
// Meta+wch.
@ -507,18 +548,17 @@ void TerminalInput::_SendNullInputSequence(const DWORD dwControlKeyState) const
}
}
void TerminalInput::_SendInputSequence(_In_ PCWSTR const pwszSequence) const
void TerminalInput::_SendInputSequence(_In_ const std::wstring_view sequence) const
{
size_t cch = 0;
// + 1 to max sequence length for null terminator count which is required by StringCchLengthW
if (SUCCEEDED(StringCchLengthW(pwszSequence, _TermKeyMap::s_cchMaxSequenceLength + 1, &cch)) && cch > 0)
const auto length = sequence.length();
if (length <= _TermKeyMap::s_cchMaxSequenceLength && length > 0)
{
try
{
std::deque<std::unique_ptr<IInputEvent>> inputEvents;
for (size_t i = 0; i < cch; i++)
for (size_t i = 0; i < length; i++)
{
inputEvents.push_back(std::make_unique<KeyEvent>(true, 1ui16, 0ui16, 0ui16, pwszSequence[i], 0));
inputEvents.push_back(std::make_unique<KeyEvent>(true, 1ui16, 0ui16, 0ui16, sequence[i], 0));
}
_pfnWriteEvents(inputEvents);
}

View File

@ -34,16 +34,21 @@ namespace Microsoft::Console::VirtualTerminal
~TerminalInput() = default;
bool HandleKey(const IInputEvent* const pInEvent) const;
bool HandleChar(const wchar_t ch);
void ChangeKeypadMode(const bool fApplicationMode);
void ChangeCursorKeysMode(const bool fApplicationMode);
private:
std::function<void(std::deque<std::unique_ptr<IInputEvent>>&)> _pfnWriteEvents;
// storage location for the leading surrogate of a utf-16 surrogate pair
std::optional<wchar_t> _leadingSurrogate;
bool _fKeypadApplicationMode = false;
bool _fCursorApplicationMode = false;
void _SendNullInputSequence(const DWORD dwControlKeyState) const;
void _SendInputSequence(_In_ PCWSTR const pwszSequence) const;
void _SendInputSequence(_In_ const std::wstring_view sequence) const;
void _SendEscapedInputSequence(const wchar_t wch) const;
struct _TermKeyMap
@ -54,12 +59,12 @@ namespace Microsoft::Console::VirtualTerminal
static const size_t s_cchMaxSequenceLength;
_TermKeyMap(const WORD wVirtualKey, _In_ PCWSTR const pwszSequence) :
_TermKeyMap(const WORD wVirtualKey, _In_ PCWSTR const pwszSequence) noexcept :
wVirtualKey(wVirtualKey),
pwszSequence(pwszSequence),
dwModifiers(0){};
_TermKeyMap(const WORD wVirtualKey, const DWORD dwModifiers, _In_ PCWSTR const pwszSequence) :
_TermKeyMap(const WORD wVirtualKey, const DWORD dwModifiers, _In_ PCWSTR const pwszSequence) noexcept :
wVirtualKey(wVirtualKey),
pwszSequence(pwszSequence),
dwModifiers(dwModifiers){};

View File

@ -18,6 +18,10 @@ namespace Microsoft::Console::VirtualTerminal
{
public:
virtual ~IStateMachineEngine() = 0;
IStateMachineEngine(const IStateMachineEngine&) = default;
IStateMachineEngine(IStateMachineEngine&&) = default;
IStateMachineEngine& operator=(const IStateMachineEngine&) = default;
IStateMachineEngine& operator=(IStateMachineEngine&&) = default;
virtual bool ActionExecute(const wchar_t wch) = 0;
virtual bool ActionExecuteFromEscape(const wchar_t wch) = 0;
@ -53,6 +57,9 @@ namespace Microsoft::Console::VirtualTerminal
virtual bool FlushAtEndOfString() const = 0;
virtual bool DispatchControlCharsFromEscape() const = 0;
virtual bool DispatchIntermediatesFromEscape() const = 0;
protected:
IStateMachineEngine() = default;
};
inline IStateMachineEngine::~IStateMachineEngine() {}