From 7bc8d4657aab8e4538a77cf3a93d1cad6a1ec2b5 Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Thu, 14 Nov 2019 16:41:54 +1300 Subject: [PATCH 01/13] Added 'rowsToScroll' setting in global settings, to configure the number of rows scrolled at a time. Default was hardcoded to 4. 1 works better on precision touchpads. --- src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp | 6 +++++- src/cascadia/TerminalApp/GlobalAppSettings.cpp | 8 ++++++++ src/cascadia/TerminalApp/GlobalAppSettings.h | 2 ++ src/cascadia/TerminalApp/defaults.json | 1 + src/cascadia/TerminalControl/TermControl.cpp | 9 ++++++--- src/cascadia/TerminalControl/TermControl.h | 2 ++ src/cascadia/TerminalSettings/ICoreSettings.idl | 2 ++ src/cascadia/TerminalSettings/TerminalSettings.cpp | 11 +++++++++++ src/cascadia/TerminalSettings/terminalsettings.h | 3 +++ .../UnitTests_TerminalCore/MockTermSettings.h | 2 ++ src/inc/DefaultSettings.h | 1 + 11 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index 9848044b255..da869de5405 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -415,7 +415,8 @@ namespace TerminalAppLocalTests "globals": { "alwaysShowTabs": true, "initialCols" : 120, - "initialRows" : 30 + "initialRows" : 30, + "rowsToScroll" : 4, } })" }; const std::string settings1String{ R"( @@ -424,6 +425,7 @@ namespace TerminalAppLocalTests "showTabsInTitlebar": false, "initialCols" : 240, "initialRows" : 60 + "rowsToScroll" : 8, } })" }; const auto settings0Json = VerifyParseSucceeded(settings0String); @@ -435,12 +437,14 @@ namespace TerminalAppLocalTests VERIFY_ARE_EQUAL(true, settings._globals._alwaysShowTabs); VERIFY_ARE_EQUAL(120, settings._globals._initialCols); VERIFY_ARE_EQUAL(30, settings._globals._initialRows); + VERIFY_ARE_EQUAL(4, settings._globals._rowsToScroll); VERIFY_ARE_EQUAL(true, settings._globals._showTabsInTitlebar); settings.LayerJson(settings1Json); VERIFY_ARE_EQUAL(true, settings._globals._alwaysShowTabs); VERIFY_ARE_EQUAL(240, settings._globals._initialCols); VERIFY_ARE_EQUAL(60, settings._globals._initialRows); + VERIFY_ARE_EQUAL(8, settings._globals._rowsToScroll); VERIFY_ARE_EQUAL(false, settings._globals._showTabsInTitlebar); } diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.cpp b/src/cascadia/TerminalApp/GlobalAppSettings.cpp index 1972c6d909b..24207faab24 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalApp/GlobalAppSettings.cpp @@ -21,6 +21,7 @@ static constexpr std::string_view DefaultProfileKey{ "defaultProfile" }; static constexpr std::string_view AlwaysShowTabsKey{ "alwaysShowTabs" }; static constexpr std::string_view InitialRowsKey{ "initialRows" }; static constexpr std::string_view InitialColsKey{ "initialCols" }; +static constexpr std::string_view RowsToScrollKey{ "rowsToScroll" }; static constexpr std::string_view InitialPositionKey{ "initialPosition" }; static constexpr std::string_view ShowTitleInTitlebarKey{ "showTerminalTitleInTitlebar" }; static constexpr std::string_view RequestedThemeKey{ "requestedTheme" }; @@ -41,6 +42,7 @@ GlobalAppSettings::GlobalAppSettings() : _alwaysShowTabs{ true }, _initialRows{ DEFAULT_ROWS }, _initialCols{ DEFAULT_COLS }, + _rowsToScroll{ DEFAULT_ROWSTOSCROLL }, _initialX{}, _initialY{}, _showTitleInTitlebar{ true }, @@ -175,6 +177,7 @@ void GlobalAppSettings::ApplyToSettings(TerminalSettings& settings) const noexce settings.KeyBindings(GetKeybindings()); settings.InitialRows(_initialRows); settings.InitialCols(_initialCols); + settings.RowsToScroll(_rowsToScroll); settings.WordDelimiters(_wordDelimiters); settings.CopyOnSelect(_copyOnSelect); @@ -193,6 +196,7 @@ Json::Value GlobalAppSettings::ToJson() const jsonObject[JsonKey(DefaultProfileKey)] = winrt::to_string(Utils::GuidToString(_defaultProfile)); jsonObject[JsonKey(InitialRowsKey)] = _initialRows; jsonObject[JsonKey(InitialColsKey)] = _initialCols; + jsonObject[JsonKey(RowsToScrollKey)] = _rowsToScroll; jsonObject[JsonKey(InitialPositionKey)] = _SerializeInitialPosition(_initialX, _initialY); jsonObject[JsonKey(AlwaysShowTabsKey)] = _alwaysShowTabs; jsonObject[JsonKey(ShowTitleInTitlebarKey)] = _showTitleInTitlebar; @@ -239,6 +243,10 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) { _initialCols = initialCols.asInt(); } + if (auto rowsToScroll{ json[JsonKey(RowsToScrollKey)] }) + { + _rowsToScroll = rowsToScroll.asInt(); + } if (auto initialPosition{ json[JsonKey(InitialPositionKey)] }) { _ParseInitialPosition(GetWstringFromJson(initialPosition), _initialX, _initialY); diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.h b/src/cascadia/TerminalApp/GlobalAppSettings.h index c6e34544d57..3d87e90322d 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.h +++ b/src/cascadia/TerminalApp/GlobalAppSettings.h @@ -85,6 +85,8 @@ class TerminalApp::GlobalAppSettings final int32_t _initialRows; int32_t _initialCols; + int32_t _rowsToScroll; + std::optional _initialX; std::optional _initialY; diff --git a/src/cascadia/TerminalApp/defaults.json b/src/cascadia/TerminalApp/defaults.json index 40960936ff2..16255af69e9 100644 --- a/src/cascadia/TerminalApp/defaults.json +++ b/src/cascadia/TerminalApp/defaults.json @@ -4,6 +4,7 @@ "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", "initialCols": 120, "initialRows": 30, + "rowsToScroll" : 4, "requestedTheme": "system", "showTabsInTitlebar": true, "showTerminalTitleInTitlebar": true, diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index a52040bc481..575d3e17dfe 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -236,6 +236,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // The Codepage is additionally not actually used by the DX engine at all. _actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false }; _desiredFont = { _actualFont }; + + _rowsToScroll = _settings.RowsToScroll(); } // Method Description: @@ -906,6 +908,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const auto point = args.GetCurrentPoint(_root); const auto delta = point.Properties().MouseWheelDelta(); + + // Get the state of the Ctrl & Shift keys // static_cast to a uint32_t because we can't use the WI_IsFlagSet macro // directly with a VirtualKeyModifiers @@ -1001,9 +1005,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // for number of lines scrolled? // With one of the precision mouses, one click is always a multiple of 120, // but the "smooth scrolling" mode results in non-int values - - // Conhost seems to use four lines at a time, so we'll emulate that for now. - double newValue = (4 * rowDelta) + (currentOffset); + // This is now populated from settings. + double newValue = (_rowsToScroll * rowDelta) + (currentOffset); // Clear our expected scroll offset. The viewport will now move in // response to our user input. diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index ee3597ba46b..1df402735d9 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -112,6 +112,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation FontInfoDesired _desiredFont; FontInfo _actualFont; + int _rowsToScroll; + std::optional _lastScrollOffset; // Auto scroll occurs when user, while selecting, drags cursor outside viewport. View is then scrolled to 'follow' the cursor. diff --git a/src/cascadia/TerminalSettings/ICoreSettings.idl b/src/cascadia/TerminalSettings/ICoreSettings.idl index b715bd70868..83bcaaaf754 100644 --- a/src/cascadia/TerminalSettings/ICoreSettings.idl +++ b/src/cascadia/TerminalSettings/ICoreSettings.idl @@ -22,6 +22,8 @@ namespace Microsoft.Terminal.Settings Int32 HistorySize; Int32 InitialRows; Int32 InitialCols; + Int32 RowsToScroll; + Boolean SnapOnInput; UInt32 CursorColor; diff --git a/src/cascadia/TerminalSettings/TerminalSettings.cpp b/src/cascadia/TerminalSettings/TerminalSettings.cpp index 70c2d4f7a5f..aecd5645125 100644 --- a/src/cascadia/TerminalSettings/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettings/TerminalSettings.cpp @@ -16,6 +16,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation _historySize{ DEFAULT_HISTORY_SIZE }, _initialRows{ 30 }, _initialCols{ 80 }, + _rowsToScroll{ 4 }, _snapOnInput{ true }, _cursorColor{ DEFAULT_CURSOR_COLOR }, _cursorShape{ CursorStyle::Vintage }, @@ -100,6 +101,16 @@ namespace winrt::Microsoft::Terminal::Settings::implementation _initialCols = value; } + int32_t TerminalSettings::RowsToScroll() + { + return _rowsToScroll; + } + + void TerminalSettings::RowsToScroll(int32_t value) + { + _rowsToScroll = value; + } + bool TerminalSettings::SnapOnInput() { return _snapOnInput; diff --git a/src/cascadia/TerminalSettings/terminalsettings.h b/src/cascadia/TerminalSettings/terminalsettings.h index c701631da8a..db2772dee49 100644 --- a/src/cascadia/TerminalSettings/terminalsettings.h +++ b/src/cascadia/TerminalSettings/terminalsettings.h @@ -37,6 +37,8 @@ namespace winrt::Microsoft::Terminal::Settings::implementation void InitialRows(int32_t value); int32_t InitialCols(); void InitialCols(int32_t value); + int32_t RowsToScroll(); + void RowsToScroll(int32_t value); bool SnapOnInput(); void SnapOnInput(bool value); uint32_t CursorColor(); @@ -101,6 +103,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation int32_t _historySize; int32_t _initialRows; int32_t _initialCols; + int32_t _rowsToScroll; bool _snapOnInput; uint32_t _cursorColor; Settings::CursorStyle _cursorShape; diff --git a/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h b/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h index eb5e4011a7c..207353c0b32 100644 --- a/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h +++ b/src/cascadia/UnitTests_TerminalCore/MockTermSettings.h @@ -25,6 +25,7 @@ namespace TerminalCoreUnitTests int32_t HistorySize() { return _historySize; } int32_t InitialRows() { return _initialRows; } int32_t InitialCols() { return _initialCols; } + int32_t RowsToScroll() { return 4; } uint32_t DefaultForeground() { return COLOR_WHITE; } uint32_t DefaultBackground() { return COLOR_BLACK; } bool SnapOnInput() { return false; } @@ -41,6 +42,7 @@ namespace TerminalCoreUnitTests void HistorySize(int32_t) {} void InitialRows(int32_t) {} void InitialCols(int32_t) {} + void RowsToScroll(int32_t) {} void DefaultForeground(uint32_t) {} void DefaultBackground(uint32_t) {} void SnapOnInput(bool) {} diff --git a/src/inc/DefaultSettings.h b/src/inc/DefaultSettings.h index ed6d8f2a2f7..0e731f044a1 100644 --- a/src/inc/DefaultSettings.h +++ b/src/inc/DefaultSettings.h @@ -35,6 +35,7 @@ constexpr int DEFAULT_FONT_SIZE = 12; constexpr int DEFAULT_ROWS = 30; constexpr int DEFAULT_COLS = 120; +constexpr int DEFAULT_ROWSTOSCROLL = 4; const std::wstring DEFAULT_PADDING{ L"8, 8, 8, 8" }; const std::wstring DEFAULT_STARTING_DIRECTORY{ L"%USERPROFILE%" }; From b7dd11ba6968bfbaa934faf1273fe033154d52c9 Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Tue, 26 Nov 2019 15:31:53 +1300 Subject: [PATCH 02/13] Ran Invoke-CodeFormat --- src/cascadia/TerminalControl/TermControl.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 8b0fb657699..2f1a3c8e582 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -921,8 +921,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const auto point = args.GetCurrentPoint(_root); const auto delta = point.Properties().MouseWheelDelta(); - - // Get the state of the Ctrl & Shift keys // static_cast to a uint32_t because we can't use the WI_IsFlagSet macro // directly with a VirtualKeyModifiers From 20c8af0031385dd0e5bb3f9acac59a0d7f6075eb Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Tue, 26 Nov 2019 20:43:05 +1300 Subject: [PATCH 03/13] Run code format --- src/cascadia/TerminalControl/TermControl.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 19a33cdaf4c..010be0d80e2 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -945,8 +945,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation const auto point = args.GetCurrentPoint(_root); const auto delta = point.Properties().MouseWheelDelta(); - - // Get the state of the Ctrl & Shift keys // static_cast to a uint32_t because we can't use the WI_IsFlagSet macro // directly with a VirtualKeyModifiers From 73367115791796a0d3a576f1bfdd4b9e212e039f Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Mon, 2 Dec 2019 17:10:16 +1300 Subject: [PATCH 04/13] Fixed whitespace in file for better diff in PR. --- src/cascadia/TerminalControl/TermControl.cpp | 3973 +++++++++--------- 1 file changed, 1987 insertions(+), 1986 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 010be0d80e2..4cf2d7afd27 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1,1986 +1,1987 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "pch.h" -#include "TermControl.h" -#include -#include -#include -#include -#include -#include -#include "..\..\types\inc\GlyphWidth.hpp" - -#include "TermControl.g.cpp" -#include "TermControlAutomationPeer.h" - -using namespace ::Microsoft::Console::Types; -using namespace ::Microsoft::Terminal::Core; -using namespace winrt::Windows::UI::Xaml; -using namespace winrt::Windows::UI::Core; -using namespace winrt::Windows::System; -using namespace winrt::Microsoft::Terminal::Settings; - -namespace winrt::Microsoft::Terminal::TerminalControl::implementation -{ - // Helper static function to ensure that all ambiguous-width glyphs are reported as narrow. - // See microsoft/terminal#2066 for more info. - static bool _IsGlyphWideForceNarrowFallback(const std::wstring_view /* glyph */) - { - return false; // glyph is not wide. - } - - static bool _EnsureStaticInitialization() - { - // use C++11 magic statics to make sure we only do this once. - static bool initialized = []() { - // *** THIS IS A SINGLETON *** - SetGlyphWidthFallback(_IsGlyphWideForceNarrowFallback); - - return true; - }(); - return initialized; - } - - TermControl::TermControl() : - TermControl(Settings::TerminalSettings{}, TerminalConnection::ITerminalConnection{ nullptr }) - { - } - - TermControl::TermControl(Settings::IControlSettings settings, TerminalConnection::ITerminalConnection connection) : - _connection{ connection }, - _initializedTerminal{ false }, - _root{ nullptr }, - _swapChainPanel{ nullptr }, - _settings{ settings }, - _closing{ false }, - _lastScrollOffset{ std::nullopt }, - _autoScrollVelocity{ 0 }, - _autoScrollingPointerPoint{ std::nullopt }, - _autoScrollTimer{}, - _lastAutoScrollUpdateTime{ std::nullopt }, - _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 }, - _cursorTimer{}, - _lastMouseClick{}, - _lastMouseClickPos{}, - _tsfInputControl{ nullptr } - { - _EnsureStaticInitialization(); - _Create(); - } - - void TermControl::_Create() - { - Controls::Grid container; - - Controls::ColumnDefinition contentColumn{}; - Controls::ColumnDefinition scrollbarColumn{}; - contentColumn.Width(GridLength{ 1.0, GridUnitType::Star }); - scrollbarColumn.Width(GridLength{ 1.0, GridUnitType::Auto }); - - container.ColumnDefinitions().Append(contentColumn); - container.ColumnDefinitions().Append(scrollbarColumn); - - _scrollBar = Controls::Primitives::ScrollBar{}; - _scrollBar.Orientation(Controls::Orientation::Vertical); - _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); - _scrollBar.HorizontalAlignment(HorizontalAlignment::Right); - _scrollBar.VerticalAlignment(VerticalAlignment::Stretch); - - // Initialize the scrollbar with some placeholder values. - // The scrollbar will be updated with real values on _Initialize - _scrollBar.Maximum(1); - _scrollBar.ViewportSize(10); - _scrollBar.IsTabStop(false); - _scrollBar.SmallChange(1); - _scrollBar.LargeChange(4); - _scrollBar.Visibility(Visibility::Visible); - - _tsfInputControl = TSFInputControl(); - _tsfInputControl.CompositionCompleted({ this, &TermControl::_CompositionCompleted }); - _tsfInputControl.CurrentCursorPosition({ this, &TermControl::_CurrentCursorPositionHandler }); - _tsfInputControl.CurrentFontInfo({ this, &TermControl::_FontInfoHandler }); - container.Children().Append(_tsfInputControl); - - // Create the SwapChainPanel that will display our content - Controls::SwapChainPanel swapChainPanel; - - _sizeChangedRevoker = swapChainPanel.SizeChanged(winrt::auto_revoke, { this, &TermControl::_SwapChainSizeChanged }); - _compositionScaleChangedRevoker = swapChainPanel.CompositionScaleChanged(winrt::auto_revoke, { this, &TermControl::_SwapChainScaleChanged }); - - // Initialize the terminal only once the swapchainpanel is loaded - that - // way, we'll be able to query the real pixel size it got on layout - _layoutUpdatedRevoker = swapChainPanel.LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { - // This event fires every time the layout changes, but it is always the last one to fire - // in any layout change chain. That gives us great flexibility in finding the right point - // at which to initialize our renderer (and our terminal). - // Any earlier than the last layout update and we may not know the terminal's starting size. - if (_InitializeTerminal()) - { - // Only let this succeed once. - this->_layoutUpdatedRevoker.revoke(); - } - }); - - container.Children().Append(swapChainPanel); - container.Children().Append(_scrollBar); - Controls::Grid::SetColumn(swapChainPanel, 0); - Controls::Grid::SetColumn(_scrollBar, 1); - - Controls::Grid root{}; - Controls::Image bgImageLayer{}; - root.Children().Append(bgImageLayer); - root.Children().Append(container); - - _root = root; - _bgImageLayer = bgImageLayer; - - _swapChainPanel = swapChainPanel; - this->Content(_root); - - _ApplyUISettings(); - - // These are important: - // 1. When we get tapped, focus us - this->Tapped([this](auto&, auto& e) { - this->Focus(FocusState::Pointer); - e.Handled(true); - }); - // 2. Make sure we can be focused (why this isn't `Focusable` I'll never know) - this->IsTabStop(true); - // 3. Actually not sure about this one. Maybe it isn't necessary either. - this->AllowFocusOnInteraction(true); - - // DON'T CALL _InitializeTerminal here - wait until the swap chain is loaded to do that. - - // Subscribe to the connection's disconnected event and call our connection closed handlers. - _connectionStateChangedRevoker = _connection.StateChanged(winrt::auto_revoke, [weakThis = get_weak()](auto&& /*s*/, auto&& /*v*/) { - if (auto strongThis{ weakThis.get() }) - { - strongThis->_ConnectionStateChangedHandlers(*strongThis, nullptr); - } - }); - } - - // Method Description: - // - Given new settings for this profile, applies the settings to the current terminal. - // Arguments: - // - newSettings: New settings values for the profile in this terminal. - // Return Value: - // - - void TermControl::UpdateSettings(Settings::IControlSettings newSettings) - { - _settings = newSettings; - - // Dispatch a call to the UI thread to apply the new settings to the - // terminal. - _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this]() { - // Update our control settings - _ApplyUISettings(); - - // Update DxEngine's SelectionBackground - _renderEngine->SetSelectionBackground(_settings.SelectionBackground()); - - // Update the terminal core with its new Core settings - _terminal->UpdateSettings(_settings); - - // Refresh our font with the renderer - _UpdateFont(); - - const auto width = _swapChainPanel.ActualWidth(); - const auto height = _swapChainPanel.ActualHeight(); - if (width != 0 && height != 0) - { - // If the font size changed, or the _swapchainPanel's size changed - // for any reason, we'll need to make sure to also resize the - // buffer. _DoResize will invalidate everything for us. - auto lock = _terminal->LockForWriting(); - _DoResize(width, height); - } - - // set TSF Foreground - Media::SolidColorBrush foregroundBrush{}; - foregroundBrush.Color(ColorRefToColor(_settings.DefaultForeground())); - _tsfInputControl.Foreground(foregroundBrush); - }); - } - - // Method Description: - // - Style our UI elements based on the values in our _settings, and set up - // other control-specific settings. This method will be called whenever - // the settings are reloaded. - // * Calls _InitializeBackgroundBrush to set up the Xaml brush responsible - // for the control's background - // * Calls _BackgroundColorChanged to style the background of the control - // - Core settings will be passed to the terminal in _InitializeTerminal - // Arguments: - // - - // Return Value: - // - - void TermControl::_ApplyUISettings() - { - _InitializeBackgroundBrush(); - - uint32_t bg = _settings.DefaultBackground(); - _BackgroundColorChanged(bg); - - // Apply padding as swapChainPanel's margin - auto newMargin = _ParseThicknessFromPadding(_settings.Padding()); - auto existingMargin = _swapChainPanel.Margin(); - _swapChainPanel.Margin(newMargin); - - if (newMargin != existingMargin && newMargin != Thickness{ 0 }) - { - TraceLoggingWrite(g_hTerminalControlProvider, - "NonzeroPaddingApplied", - TraceLoggingDescription("An event emitted when a control has padding applied to it"), - TraceLoggingStruct(4, "Padding"), - TraceLoggingFloat64(newMargin.Left, "Left"), - TraceLoggingFloat64(newMargin.Top, "Top"), - TraceLoggingFloat64(newMargin.Right, "Right"), - TraceLoggingFloat64(newMargin.Bottom, "Bottom"), - TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), - TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); - } - - // Initialize our font information. - const auto* fontFace = _settings.FontFace().c_str(); - const short fontHeight = gsl::narrow(_settings.FontSize()); - // The font width doesn't terribly matter, we'll only be using the - // height to look it up - // The other params here also largely don't matter. - // The family is only used to determine if the font is truetype or - // not, but DX doesn't use that info at all. - // The Codepage is additionally not actually used by the DX engine at all. - _actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false }; - _desiredFont = { _actualFont }; - - _rowsToScroll = _settings.RowsToScroll(); - // set TSF Foreground - Media::SolidColorBrush foregroundBrush{}; - foregroundBrush.Color(ColorRefToColor(_settings.DefaultForeground())); - _tsfInputControl.Foreground(foregroundBrush); - _tsfInputControl.Margin(newMargin); - } - - // Method Description: - // - Set up each layer's brush used to display the control's background. - // - Respects the settings for acrylic, background image and opacity from - // _settings. - // * If acrylic is not enabled, setup a solid color background, otherwise - // use bgcolor as acrylic's tint - // - Avoids image flickering and acrylic brush redraw if settings are changed - // but the appropriate brush is still in place. - // - Does not apply background color outside of acrylic mode; - // _BackgroundColorChanged must be called to do so. - // Arguments: - // - - // Return Value: - // - - void TermControl::_InitializeBackgroundBrush() - { - if (_settings.UseAcrylic()) - { - // See if we've already got an acrylic background brush - // to avoid the flicker when setting up a new one - auto acrylic = _root.Background().try_as(); - - // Instantiate a brush if there's not already one there - if (acrylic == nullptr) - { - acrylic = Media::AcrylicBrush{}; - acrylic.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); - } - - // see GH#1082: Initialize background color so we don't get a - // fade/flash when _BackgroundColorChanged is called - uint32_t color = _settings.DefaultBackground(); - winrt::Windows::UI::Color bgColor{}; - bgColor.R = GetRValue(color); - bgColor.G = GetGValue(color); - bgColor.B = GetBValue(color); - bgColor.A = 255; - - acrylic.FallbackColor(bgColor); - acrylic.TintColor(bgColor); - - // Apply brush settings - acrylic.TintOpacity(_settings.TintOpacity()); - - // Apply brush to control if it's not already there - if (_root.Background() != acrylic) - { - _root.Background(acrylic); - } - } - else - { - Media::SolidColorBrush solidColor{}; - _root.Background(solidColor); - } - - if (!_settings.BackgroundImage().empty()) - { - Windows::Foundation::Uri imageUri{ _settings.BackgroundImage() }; - - // Check if the image brush is already pointing to the image - // in the modified settings; if it isn't (or isn't there), - // set a new image source for the brush - auto imageSource = _bgImageLayer.Source().try_as(); - - if (imageSource == nullptr || - imageSource.UriSource() == nullptr || - imageSource.UriSource().RawUri() != imageUri.RawUri()) - { - // Note that BitmapImage handles the image load asynchronously, - // which is especially important since the image - // may well be both large and somewhere out on the - // internet. - Media::Imaging::BitmapImage image(imageUri); - _bgImageLayer.Source(image); - } - - // Apply stretch, opacity and alignment settings - _bgImageLayer.Stretch(_settings.BackgroundImageStretchMode()); - _bgImageLayer.Opacity(_settings.BackgroundImageOpacity()); - _bgImageLayer.HorizontalAlignment(_settings.BackgroundImageHorizontalAlignment()); - _bgImageLayer.VerticalAlignment(_settings.BackgroundImageVerticalAlignment()); - } - else - { - _bgImageLayer.Source(nullptr); - } - } - - // Method Description: - // - Style the background of the control with the provided background color - // Arguments: - // - color: The background color to use as a uint32 (aka DWORD COLORREF) - // Return Value: - // - - void TermControl::_BackgroundColorChanged(const uint32_t color) - { - _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this, color]() { - const auto R = GetRValue(color); - const auto G = GetGValue(color); - const auto B = GetBValue(color); - - winrt::Windows::UI::Color bgColor{}; - bgColor.R = R; - bgColor.G = G; - bgColor.B = B; - bgColor.A = 255; - - if (auto acrylic = _root.Background().try_as()) - { - acrylic.FallbackColor(bgColor); - acrylic.TintColor(bgColor); - } - else if (auto solidColor = _root.Background().try_as()) - { - solidColor.Color(bgColor); - } - - // Set the default background as transparent to prevent the - // DX layer from overwriting the background image or acrylic effect - _settings.DefaultBackground(ARGB(0, R, G, B)); - }); - } - - TermControl::~TermControl() - { - Close(); - } - - Windows::UI::Xaml::Automation::Peers::AutomationPeer TermControl::OnCreateAutomationPeer() - { - Windows::UI::Xaml::Automation::Peers::AutomationPeer autoPeer{ nullptr }; - if (GetUiaData()) - { - try - { - // create a custom automation peer with this code pattern: - // (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers) - autoPeer = winrt::make(this); - } - CATCH_LOG(); - } - return autoPeer; - } - - ::Microsoft::Console::Types::IUiaData* TermControl::GetUiaData() const - { - return _terminal.get(); - } - - const FontInfo TermControl::GetActualFont() const - { - return _actualFont; - } - - const Windows::UI::Xaml::Thickness TermControl::GetPadding() const - { - return _swapChainPanel.Margin(); - } - - TerminalConnection::ConnectionState TermControl::ConnectionState() const - { - return _connection.State(); - } - - void TermControl::SwapChainChanged() - { - if (!_initializedTerminal) - { - return; - } - - auto chain = _renderEngine->GetSwapChain(); - _swapChainPanel.Dispatcher().RunAsync(CoreDispatcherPriority::High, [=]() { - auto lock = _terminal->LockForWriting(); - auto nativePanel = _swapChainPanel.as(); - nativePanel->SetSwapChain(chain.Get()); - }); - } - - bool TermControl::_InitializeTerminal() - { - if (_initializedTerminal) - { - return false; - } - - const auto windowWidth = _swapChainPanel.ActualWidth(); // Width() and Height() are NaN? - const auto windowHeight = _swapChainPanel.ActualHeight(); - - if (windowWidth == 0 || windowHeight == 0) - { - return false; - } - - _terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>(); - - // First create the render thread. - // Then stash a local pointer to the render thread so we can initialize it and enable it - // to paint itself *after* we hand off its ownership to the renderer. - // We split up construction and initialization of the render thread object this way - // because the renderer and render thread have circular references to each other. - auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>(); - auto* const localPointerToThread = renderThread.get(); - - // Now create the renderer and initialize the render thread. - _renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread)); - ::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer; - - THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); - - // Set up the DX Engine - auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); - _renderer->AddRenderEngine(dxEngine.get()); - - // Initialize our font with the renderer - // We don't have to care about DPI. We'll get a change message immediately if it's not 96 - // and react accordingly. - _UpdateFont(); - - const COORD windowSize{ static_cast(windowWidth), static_cast(windowHeight) }; - - // Fist set up the dx engine with the window size in pixels. - // Then, using the font, get the number of characters that can fit. - // Resize our terminal connection to match that size, and initialize the terminal with that size. - const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize); - THROW_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); - - // Update DxEngine's SelectionBackground - dxEngine->SetSelectionBackground(_settings.SelectionBackground()); - - const auto vp = dxEngine->GetViewportInCharacters(viewInPixels); - const auto width = vp.Width(); - const auto height = vp.Height(); - _connection.Resize(height, width); - - // Override the default width and height to match the size of the swapChainPanel - _settings.InitialCols(width); - _settings.InitialRows(height); - - _terminal->CreateFromSettings(_settings, renderTarget); - - // Tell the DX Engine to notify us when the swap chain changes. - dxEngine->SetCallback(std::bind(&TermControl::SwapChainChanged, this)); - - THROW_IF_FAILED(dxEngine->Enable()); - _renderEngine = std::move(dxEngine); - - auto onRecieveOutputFn = [this](const hstring str) { - _terminal->Write(str.c_str()); - }; - _connectionOutputEventToken = _connection.TerminalOutput(onRecieveOutputFn); - - auto inputFn = std::bind(&TermControl::_SendInputToConnection, this, std::placeholders::_1); - _terminal->SetWriteInputCallback(inputFn); - - auto chain = _renderEngine->GetSwapChain(); - _swapChainPanel.Dispatcher().RunAsync(CoreDispatcherPriority::High, [this, chain]() { - _terminal->LockConsole(); - auto nativePanel = _swapChainPanel.as(); - nativePanel->SetSwapChain(chain.Get()); - _terminal->UnlockConsole(); - }); - - // Set up the height of the ScrollViewer and the grid we're using to fake our scrolling height - auto bottom = _terminal->GetViewport().BottomExclusive(); - auto bufferHeight = bottom; - - const auto originalMaximum = _scrollBar.Maximum(); - const auto originalMinimum = _scrollBar.Minimum(); - const auto originalValue = _scrollBar.Value(); - const auto originalViewportSize = _scrollBar.ViewportSize(); - - _scrollBar.Maximum(bufferHeight - bufferHeight); - _scrollBar.Minimum(0); - _scrollBar.Value(0); - _scrollBar.ViewportSize(bufferHeight); - _scrollBar.ValueChanged({ this, &TermControl::_ScrollbarChangeHandler }); - _scrollBar.PointerPressed({ this, &TermControl::_CapturePointer }); - _scrollBar.PointerReleased({ this, &TermControl::_ReleasePointerCapture }); - - // Apply settings for scrollbar - if (_settings.ScrollState() == ScrollbarState::Visible) - { - _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); - } - else if (_settings.ScrollState() == ScrollbarState::Hidden) - { - _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::None); - - // In the scenario where the user has turned off the OS setting to automatically hide scollbars, the - // Terminal scrollbar would still be visible; so, we need to set the control's visibility accordingly to - // achieve the intended effect. - _scrollBar.Visibility(Visibility::Collapsed); - } - else - { - // Default behavior - _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); - } - - _root.PointerWheelChanged({ this, &TermControl::_MouseWheelHandler }); - - // These need to be hooked up to the SwapChainPanel because we don't want the scrollbar to respond to pointer events (GitHub #950) - _swapChainPanel.PointerPressed({ this, &TermControl::_PointerPressedHandler }); - _swapChainPanel.PointerMoved({ this, &TermControl::_PointerMovedHandler }); - _swapChainPanel.PointerReleased({ this, &TermControl::_PointerReleasedHandler }); - - localPointerToThread->EnablePainting(); - - // No matter what order these guys are in, The KeyDown's will fire - // before the CharacterRecieved, so we can't easily get characters - // first, then fallback to getting keys from vkeys. - // TODO: This apparently handles keys and characters correctly, though - // I'd keep an eye on it, and test more. - // I presume that the characters that aren't translated by terminalInput - // just end up getting ignored, and the rest of the input comes - // through CharacterRecieved. - // I don't believe there's a difference between KeyDown and - // PreviewKeyDown for our purposes - // These two handlers _must_ be on this, not _root. - this->PreviewKeyDown({ this, &TermControl::_KeyDownHandler }); - this->CharacterReceived({ this, &TermControl::_CharacterHandler }); - - auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1); - _terminal->SetTitleChangedCallback(pfnTitleChanged); - - auto pfnBackgroundColorChanged = std::bind(&TermControl::_BackgroundColorChanged, this, std::placeholders::_1); - _terminal->SetBackgroundCallback(pfnBackgroundColorChanged); - - auto pfnScrollPositionChanged = std::bind(&TermControl::_TerminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); - _terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged); - - static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast(1.0 / 30.0 * 1000000)); - _autoScrollTimer.Interval(AutoScrollUpdateInterval); - _autoScrollTimer.Tick({ this, &TermControl::_UpdateAutoScroll }); - - // Set up blinking cursor - int blinkTime = GetCaretBlinkTime(); - if (blinkTime != INFINITE) - { - // Create a timer - _cursorTimer = std::make_optional(DispatcherTimer()); - _cursorTimer.value().Interval(std::chrono::milliseconds(blinkTime)); - _cursorTimer.value().Tick({ this, &TermControl::_BlinkCursor }); - _cursorTimer.value().Start(); - } - else - { - // The user has disabled cursor blinking - _cursorTimer = std::nullopt; - } - - // import value from WinUser (convert from milli-seconds to micro-seconds) - _multiClickTimer = GetDoubleClickTime() * 1000; - - _gotFocusRevoker = this->GotFocus(winrt::auto_revoke, { this, &TermControl::_GotFocusHandler }); - _lostFocusRevoker = this->LostFocus(winrt::auto_revoke, { this, &TermControl::_LostFocusHandler }); - - // Focus the control here. If we do it up above (in _Create_), then the - // focus won't actually get passed to us. I believe this is because - // we're not technically a part of the UI tree yet, so focusing us - // becomes a no-op. - this->Focus(FocusState::Programmatic); - - _connection.Start(); - _initializedTerminal = true; - return true; - } - - void TermControl::_CharacterHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, - Input::CharacterReceivedRoutedEventArgs const& e) - { - if (_closing) - { - return; - } - - const auto ch = e.Character(); - - const bool handled = _terminal->SendCharEvent(ch); - e.Handled(handled); - } - - void TermControl::_KeyDownHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, - Input::KeyRoutedEventArgs const& e) - { - // mark event as handled and do nothing if... - // - closing - // - key modifier is pressed - // NOTE: for key combos like CTRL + C, two events are fired (one for CTRL, one for 'C'). We care about the 'C' event and then check for key modifiers below. - if (_closing || - e.OriginalKey() == VirtualKey::Control || - e.OriginalKey() == VirtualKey::Shift || - e.OriginalKey() == VirtualKey::Menu || - e.OriginalKey() == VirtualKey::LeftWindows || - e.OriginalKey() == VirtualKey::RightWindows) - - { - e.Handled(true); - return; - } - - const auto modifiers = _GetPressedModifierKeys(); - const auto vkey = gsl::narrow_cast(e.OriginalKey()); - const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); - bool handled = false; - - // GH#2235: Terminal::Settings hasn't been modified to differentiate between AltGr and Ctrl+Alt yet. - // -> Don't check for key bindings if this is an AltGr key combination. - if (!modifiers.IsAltGrPressed()) - { - auto bindings = _settings.KeyBindings(); - if (bindings) - { - handled = bindings.TryKeyChord({ - modifiers.IsCtrlPressed(), - modifiers.IsAltPressed(), - modifiers.IsShiftPressed(), - vkey, - }); - } - } - - if (!handled) - { - handled = _TrySendKeyEvent(vkey, scanCode, modifiers); - } - - // Manually prevent keyboard navigation with tab. We want to send tab to - // the terminal, and we don't want to be able to escape focus of the - // control with tab. - if (e.OriginalKey() == VirtualKey::Tab) - { - handled = true; - } - - e.Handled(handled); - } - - // Method Description: - // - Send this particular key event to the terminal. - // See Terminal::SendKeyEvent for more information. - // - Clears the current selection. - // - Makes the cursor briefly visible during typing. - // Arguments: - // - vkey: The vkey of the key pressed. - // - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. - bool TermControl::_TrySendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates modifiers) - { - _terminal->ClearSelection(); - - // If the terminal translated the key, mark the event as handled. - // This will prevent the system from trying to get the character out - // of it and sending us a CharacterRecieved event. - const auto handled = vkey ? _terminal->SendKeyEvent(vkey, scanCode, modifiers) : true; - - if (_cursorTimer.has_value()) - { - // Manually show the cursor when a key is pressed. Restarting - // the timer prevents flickering. - _terminal->SetCursorVisible(true); - _cursorTimer.value().Start(); - } - - return handled; - } - - // Method Description: - // - handle a mouse click event. Begin selection process. - // Arguments: - // - sender: the XAML element responding to the pointer input - // - args: event data - void TermControl::_PointerPressedHandler(Windows::Foundation::IInspectable const& sender, - Input::PointerRoutedEventArgs const& args) - { - _CapturePointer(sender, args); - - const auto ptr = args.Pointer(); - const auto point = args.GetCurrentPoint(_root); - - if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) - { - // Ignore mouse events while the terminal does not have focus. - // This prevents the user from selecting and copying text if they - // click inside the current tab to refocus the terminal window. - if (!_focused) - { - args.Handled(true); - return; - } - - const auto modifiers = static_cast(args.KeyModifiers()); - // static_cast to a uint32_t because we can't use the WI_IsFlagSet - // macro directly with a VirtualKeyModifiers - const auto altEnabled = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Menu)); - const auto shiftEnabled = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Shift)); - - if (point.Properties().IsLeftButtonPressed()) - { - const auto cursorPosition = point.Position(); - const auto terminalPosition = _GetTerminalPosition(cursorPosition); - - // handle ALT key - _terminal->SetBoxSelection(altEnabled); - - auto clickCount = _NumberOfClicks(cursorPosition, point.Timestamp()); - - // 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->TripleClickSelection(terminalPosition); - _renderer->TriggerSelection(); - } - else if (multiClickMapper == 2) - { - _terminal->DoubleClickSelection(terminalPosition); - _renderer->TriggerSelection(); - } - else - { - // save location before rendering - _terminal->SetSelectionAnchor(terminalPosition); - - _renderer->TriggerSelection(); - _lastMouseClick = point.Timestamp(); - _lastMouseClickPos = cursorPosition; - } - } - else if (point.Properties().IsRightButtonPressed()) - { - // copyOnSelect causes right-click to always paste - if (_terminal->IsCopyOnSelectActive() || !_terminal->IsSelectionActive()) - { - PasteTextFromClipboard(); - } - else - { - CopySelectionToClipboard(!shiftEnabled); - } - } - } - else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) - { - const auto contactRect = point.Properties().ContactRect(); - // Set our touch rect, to start a pan. - _touchAnchor = winrt::Windows::Foundation::Point{ contactRect.X, contactRect.Y }; - } - - args.Handled(true); - } - - // Method Description: - // - handle a mouse moved event. Specifically handling mouse drag to update selection process. - // Arguments: - // - sender: not used - // - args: event data - void TermControl::_PointerMovedHandler(Windows::Foundation::IInspectable const& /*sender*/, - Input::PointerRoutedEventArgs const& args) - { - const auto ptr = args.Pointer(); - const auto point = args.GetCurrentPoint(_root); - - if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) - { - if (point.Properties().IsLeftButtonPressed()) - { - const auto cursorPosition = point.Position(); - _SetEndSelectionPointAtCursor(cursorPosition); - - const double cursorBelowBottomDist = cursorPosition.Y - _swapChainPanel.Margin().Top - _swapChainPanel.ActualHeight(); - const double cursorAboveTopDist = -1 * cursorPosition.Y + _swapChainPanel.Margin().Top; - - constexpr double MinAutoScrollDist = 2.0; // Arbitrary value - double newAutoScrollVelocity = 0.0; - if (cursorBelowBottomDist > MinAutoScrollDist) - { - newAutoScrollVelocity = _GetAutoScrollSpeed(cursorBelowBottomDist); - } - else if (cursorAboveTopDist > MinAutoScrollDist) - { - newAutoScrollVelocity = -1.0 * _GetAutoScrollSpeed(cursorAboveTopDist); - } - - if (newAutoScrollVelocity != 0) - { - _TryStartAutoScroll(point, newAutoScrollVelocity); - } - else - { - _TryStopAutoScroll(ptr.PointerId()); - } - } - } - else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch && _touchAnchor) - { - const auto contactRect = point.Properties().ContactRect(); - winrt::Windows::Foundation::Point newTouchPoint{ contactRect.X, contactRect.Y }; - const auto anchor = _touchAnchor.value(); - - // Get the difference between the point we've dragged to and the start of the touch. - const float fontHeight = float(_actualFont.GetSize().Y); - - const float dy = newTouchPoint.Y - anchor.Y; - - // If we've moved more than one row of text, we'll want to scroll the viewport - if (std::abs(dy) > fontHeight) - { - // Multiply by -1, because moving the touch point down will - // create a positive delta, but we want the viewport to move up, - // so we'll need a negative scroll amount (and the inverse for - // panning down) - const float numRows = -1.0f * (dy / fontHeight); - - const auto currentOffset = this->GetScrollOffset(); - const double newValue = (numRows) + (currentOffset); - - // Clear our expected scroll offset. The viewport will now move - // in response to our user input. - _lastScrollOffset = std::nullopt; - _scrollBar.Value(static_cast(newValue)); - - // Use this point as our new scroll anchor. - _touchAnchor = newTouchPoint; - } - } - args.Handled(true); - } - - // Method Description: - // - Event handler for the PointerReleased event. We use this to de-anchor - // touch events, to stop scrolling via touch. - // Arguments: - // - sender: the XAML element responding to the pointer input - // - args: event data - void TermControl::_PointerReleasedHandler(Windows::Foundation::IInspectable const& sender, - Input::PointerRoutedEventArgs const& args) - { - _ReleasePointerCapture(sender, args); - - const auto ptr = args.Pointer(); - - if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) - { - const auto modifiers = static_cast(args.KeyModifiers()); - // static_cast to a uint32_t because we can't use the WI_IsFlagSet - // macro directly with a VirtualKeyModifiers - const auto shiftEnabled = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Shift)); - - if (_terminal->IsCopyOnSelectActive()) - { - CopySelectionToClipboard(!shiftEnabled); - } - } - else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) - { - _touchAnchor = std::nullopt; - } - - _TryStopAutoScroll(ptr.PointerId()); - - args.Handled(true); - } - - // Method Description: - // - Event handler for the PointerWheelChanged event. This is raised in - // response to mouse wheel changes. Depending upon what modifier keys are - // pressed, different actions will take place. - // Arguments: - // - args: the event args containing information about t`he mouse wheel event. - void TermControl::_MouseWheelHandler(Windows::Foundation::IInspectable const& /*sender*/, - Input::PointerRoutedEventArgs const& args) - { - const auto point = args.GetCurrentPoint(_root); - const auto delta = point.Properties().MouseWheelDelta(); - - // Get the state of the Ctrl & Shift keys - // static_cast to a uint32_t because we can't use the WI_IsFlagSet macro - // directly with a VirtualKeyModifiers - const auto modifiers = static_cast(args.KeyModifiers()); - const auto ctrlPressed = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Control)); - const auto shiftPressed = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Shift)); - - if (ctrlPressed && shiftPressed) - { - _MouseTransparencyHandler(delta); - } - else if (ctrlPressed) - { - _MouseZoomHandler(delta); - } - else - { - _MouseScrollHandler(delta, point); - } - } - - // Method Description: - // - Adjust the opacity of the acrylic background in response to a mouse - // scrolling event. - // Arguments: - // - mouseDelta: the mouse wheel delta that triggered this event. - void TermControl::_MouseTransparencyHandler(const double mouseDelta) - { - // Transparency is on a scale of [0.0,1.0], so only increment by .01. - const auto effectiveDelta = mouseDelta < 0 ? -.01 : .01; - - if (_settings.UseAcrylic()) - { - try - { - auto acrylicBrush = _root.Background().as(); - acrylicBrush.TintOpacity(acrylicBrush.TintOpacity() + effectiveDelta); - } - CATCH_LOG(); - } - } - - // Method Description: - // - Adjust the font size of the terminal in response to a mouse scrolling - // event. - // Arguments: - // - mouseDelta: the mouse wheel delta that triggered this event. - void TermControl::_MouseZoomHandler(const double mouseDelta) - { - const auto fontDelta = mouseDelta < 0 ? -1 : 1; - AdjustFontSize(fontDelta); - } - - // Method Description: - // - Reset the font size of the terminal to its default size. - // Arguments: - // - none - void TermControl::ResetFontSize() - { - _SetFontSize(_settings.FontSize()); - } - - // Method Description: - // - Adjust the font size of the terminal control. - // Arguments: - // - fontSizeDelta: The amount to increase or decrease the font size by. - void TermControl::AdjustFontSize(int fontSizeDelta) - { - const auto newSize = _desiredFont.GetEngineSize().Y + fontSizeDelta; - _SetFontSize(newSize); - } - - // Method Description: - // - Scroll the visible viewport in response to a mouse wheel event. - // Arguments: - // - mouseDelta: the mouse wheel delta that triggered this event. - void TermControl::_MouseScrollHandler(const double mouseDelta, Windows::UI::Input::PointerPoint const& pointerPoint) - { - const auto currentOffset = this->GetScrollOffset(); - - // negative = down, positive = up - // However, for us, the signs are flipped. - const auto rowDelta = mouseDelta < 0 ? 1.0 : -1.0; - - // TODO: Should we be getting some setting from the system - // for number of lines scrolled? - // With one of the precision mouses, one click is always a multiple of 120, - // but the "smooth scrolling" mode results in non-int values - // This is now populated from settings. - double newValue = (_rowsToScroll * rowDelta) + (currentOffset); - - // Clear our expected scroll offset. The viewport will now move in - // response to our user input. - _lastScrollOffset = std::nullopt; - // The scroll bar's ValueChanged handler will actually move the viewport - // for us. - _scrollBar.Value(static_cast(newValue)); - - if (_terminal->IsSelectionActive() && pointerPoint.Properties().IsLeftButtonPressed()) - { - // If user is mouse selecting and scrolls, they then point at new character. - // Make sure selection reflects that immediately. - _SetEndSelectionPointAtCursor(pointerPoint.Position()); - } - } - - void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/, - Controls::Primitives::RangeBaseValueChangedEventArgs const& args) - { - const auto newValue = args.NewValue(); - - // If we've stored a lastScrollOffset, that means the terminal has - // initiated some scrolling operation. We're responding to that event here. - if (_lastScrollOffset.has_value()) - { - // If this event's offset is the same as the last offset message - // we've sent, then clear out the expected offset. We do this - // because in that case, the message we're replying to was the - // last scroll event we raised. - // Regardless, we're going to ignore this message, because the - // terminal is already in the scroll position it wants. - const auto ourLastOffset = _lastScrollOffset.value(); - if (newValue == ourLastOffset) - { - _lastScrollOffset = std::nullopt; - } - } - else - { - // This is a scroll event that wasn't initiated by the termnial - // itself - it was initiated by the mouse wheel, or the scrollbar. - this->ScrollViewport(static_cast(newValue)); - } - } - - // Method Description: - // - captures the pointer so that none of the other XAML elements respond to pointer events - // Arguments: - // - sender: XAML element that is interacting with pointer - // - args: pointer data (i.e.: mouse, touch) - // Return Value: - // - true if we successfully capture the pointer, false otherwise. - bool TermControl::_CapturePointer(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& args) - { - IUIElement uielem; - if (sender.try_as(uielem)) - { - uielem.CapturePointer(args.Pointer()); - return true; - } - return false; - } - - // Method Description: - // - releases the captured pointer because we're done responding to XAML pointer events - // Arguments: - // - sender: XAML element that is interacting with pointer - // - args: pointer data (i.e.: mouse, touch) - // Return Value: - // - true if we release capture of the pointer, false otherwise. - bool TermControl::_ReleasePointerCapture(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& args) - { - IUIElement uielem; - if (sender.try_as(uielem)) - { - uielem.ReleasePointerCapture(args.Pointer()); - return true; - } - return false; - } - - // Method Description: - // - Starts new pointer related auto scroll behavior, or continues existing one. - // Does nothing when there is already auto scroll associated with another pointer. - // Arguments: - // - pointerPoint: info about pointer that causes auto scroll. Pointer's position - // is later used to update selection. - // - scrollVelocity: target velocity of scrolling in characters / sec - void TermControl::_TryStartAutoScroll(Windows::UI::Input::PointerPoint const& pointerPoint, const double scrollVelocity) - { - // Allow only one pointer at the time - if (!_autoScrollingPointerPoint.has_value() || _autoScrollingPointerPoint.value().PointerId() == pointerPoint.PointerId()) - { - _autoScrollingPointerPoint = pointerPoint; - _autoScrollVelocity = scrollVelocity; - - // If this is first time the auto scroll update is about to be called, - // kick-start it by initializing its time delta as if it started now - if (!_lastAutoScrollUpdateTime.has_value()) - { - _lastAutoScrollUpdateTime = std::chrono::high_resolution_clock::now(); - } - - // Apparently this check is not necessary but greatly improves performance - if (!_autoScrollTimer.IsEnabled()) - { - _autoScrollTimer.Start(); - } - } - } - - // Method Description: - // - Stops auto scroll if it's active and is associated with supplied pointer id. - // Arguments: - // - pointerId: id of pointer for which to stop auto scroll - void TermControl::_TryStopAutoScroll(const uint32_t pointerId) - { - if (_autoScrollingPointerPoint.has_value() && pointerId == _autoScrollingPointerPoint.value().PointerId()) - { - _autoScrollingPointerPoint = std::nullopt; - _autoScrollVelocity = 0; - _lastAutoScrollUpdateTime = std::nullopt; - - // Apparently this check is not necessary but greatly improves performance - if (_autoScrollTimer.IsEnabled()) - { - _autoScrollTimer.Stop(); - } - } - } - - // Method Description: - // - Called continuously to gradually scroll viewport when user is - // mouse selecting outside it (to 'follow' the cursor). - // Arguments: - // - none - void TermControl::_UpdateAutoScroll(Windows::Foundation::IInspectable const& /* sender */, - Windows::Foundation::IInspectable const& /* e */) - { - if (_autoScrollVelocity != 0) - { - const auto timeNow = std::chrono::high_resolution_clock::now(); - - if (_lastAutoScrollUpdateTime.has_value()) - { - static constexpr double microSecPerSec = 1000000.0; - const double deltaTime = std::chrono::duration_cast(timeNow - _lastAutoScrollUpdateTime.value()).count() / microSecPerSec; - _scrollBar.Value(_scrollBar.Value() + _autoScrollVelocity * deltaTime); - - if (_autoScrollingPointerPoint.has_value()) - { - _SetEndSelectionPointAtCursor(_autoScrollingPointerPoint.value().Position()); - } - } - - _lastAutoScrollUpdateTime = timeNow; - } - } - - // Method Description: - // - Event handler for the GotFocus event. This is used to start - // blinking the cursor when the window is focused. - void TermControl::_GotFocusHandler(Windows::Foundation::IInspectable const& /* sender */, - RoutedEventArgs const& /* args */) - { - if (_closing) - { - return; - } - _focused = true; - - if (_tsfInputControl != nullptr) - { - _tsfInputControl.NotifyFocusEnter(); - } - - if (_cursorTimer.has_value()) - { - _cursorTimer.value().Start(); - } - } - - // Method Description: - // - Event handler for the LostFocus event. This is used to hide - // and stop blinking the cursor when the window loses focus. - void TermControl::_LostFocusHandler(Windows::Foundation::IInspectable const& /* sender */, - RoutedEventArgs const& /* args */) - { - if (_closing) - { - return; - } - _focused = false; - - if (_tsfInputControl != nullptr) - { - _tsfInputControl.NotifyFocusLeave(); - } - - if (_cursorTimer.has_value()) - { - _cursorTimer.value().Stop(); - _terminal->SetCursorVisible(false); - } - } - - void TermControl::_SendInputToConnection(const std::wstring& wstr) - { - _connection.WriteInput(wstr); - } - - // Method Description: - // - Pre-process text pasted (presumably from the clipboard) - // before sending it over the terminal's connection, converting - // Windows-space \r\n line-endings to \r line-endings - void TermControl::_SendPastedTextToConnection(const std::wstring& wstr) - { - // Some notes on this implementation: - // - // - std::regex can do this in a single line, but is somewhat - // overkill for a simple search/replace operation (and its - // performance guarantees aren't exactly stellar) - // - The STL doesn't have a simple string search/replace method. - // This fact is lamentable. - // - This line-ending converstion is intentionally fairly - // conservative, to avoid stripping out lone \n characters - // where they could conceivably be intentional. - - std::wstring stripped{ wstr }; - - std::wstring::size_type pos = 0; - - while ((pos = stripped.find(L"\r\n", pos)) != std::wstring::npos) - { - stripped.replace(pos, 2, L"\r"); - } - - _connection.WriteInput(stripped); - _terminal->TrySnapOnInput(); - } - - // Method Description: - // - Update the font with the renderer. This will be called either when the - // font changes or the DPI changes, as DPI changes will necessitate a - // font change. This method will *not* change the buffer/viewport size - // to account for the new glyph dimensions. Callers should make sure to - // appropriately call _DoResize after this method is called. - void TermControl::_UpdateFont() - { - auto lock = _terminal->LockForWriting(); - - const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * _swapChainPanel.CompositionScaleX()); - - // 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); - } - - // Method Description: - // - Set the font size of the terminal control. - // Arguments: - // - fontSize: The size of the font. - void TermControl::_SetFontSize(int fontSize) - { - try - { - // Make sure we have a non-zero font size - const auto newSize = std::max(gsl::narrow(fontSize), static_cast(1)); - const auto* fontFace = _settings.FontFace().c_str(); - _actualFont = { fontFace, 0, 10, { 0, newSize }, CP_UTF8, false }; - _desiredFont = { _actualFont }; - - // Refresh our font with the renderer - _UpdateFont(); - // Resize the terminal's BUFFER to match the new font size. This does - // NOT change the size of the window, because that can lead to more - // problems (like what happens when you change the font size while the - // window is maximized?) - auto lock = _terminal->LockForWriting(); - _DoResize(_swapChainPanel.ActualWidth(), _swapChainPanel.ActualHeight()); - } - CATCH_LOG(); - } - - // Method Description: - // - Triggered when the swapchain changes size. We use this to resize the - // terminal buffers to match the new visible size. - // Arguments: - // - e: a SizeChangedEventArgs with the new dimensions of the SwapChainPanel - void TermControl::_SwapChainSizeChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, - SizeChangedEventArgs const& e) - { - if (!_initializedTerminal) - { - return; - } - - auto lock = _terminal->LockForWriting(); - - const auto foundationSize = e.NewSize(); - - _DoResize(foundationSize.Width, foundationSize.Height); - } - - void TermControl::_SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, - Windows::Foundation::IInspectable const& /*args*/) - { - const auto scale = sender.CompositionScaleX(); - const auto dpi = (int)(scale * USER_DEFAULT_SCREEN_DPI); - - // TODO: MSFT: 21169071 - Shouldn't this all happen through _renderer and trigger the invalidate automatically on DPI change? - THROW_IF_FAILED(_renderEngine->UpdateDpi(dpi)); - _renderer->TriggerRedrawAll(); - } - - // Method Description: - // - Toggle the cursor on and off when called by the cursor blink timer. - // Arguments: - // - sender: not used - // - e: not used - void TermControl::_BlinkCursor(Windows::Foundation::IInspectable const& /* sender */, - Windows::Foundation::IInspectable const& /* e */) - { - if ((_closing) || (!_terminal->IsCursorBlinkingAllowed() && _terminal->IsCursorVisible())) - { - return; - } - _terminal->SetCursorVisible(!_terminal->IsCursorVisible()); - } - - // Method Description: - // - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging. - // Arguments: - // - cursorPosition: in pixels, relative to the origin of the control - void TermControl::_SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition) - { - auto terminalPosition = _GetTerminalPosition(cursorPosition); - - const short lastVisibleRow = std::max(_terminal->GetViewport().Height() - 1, 0); - const short lastVisibleCol = std::max(_terminal->GetViewport().Width() - 1, 0); - - terminalPosition.Y = std::clamp(terminalPosition.Y, 0, lastVisibleRow); - terminalPosition.X = std::clamp(terminalPosition.X, 0, lastVisibleCol); - - // save location (for rendering) + render - _terminal->SetEndSelectionPosition(terminalPosition); - _renderer->TriggerSelection(); - } - - // Method Description: - // - Process a resize event that was initiated by the user. This can either - // be due to the user resizing the window (causing the swapchain to - // resize) or due to the DPI changing (causing us to need to resize the - // buffer to match) - // Arguments: - // - newWidth: the new width of the swapchain, in pixels. - // - newHeight: the new height of the swapchain, in pixels. - void TermControl::_DoResize(const double newWidth, const double newHeight) - { - SIZE size; - size.cx = static_cast(newWidth); - size.cy = static_cast(newHeight); - - // Don't actually resize so small that a single character wouldn't fit - // in either dimension. The buffer really doesn't like being size 0. - if (size.cx < _actualFont.GetSize().X || size.cy < _actualFont.GetSize().Y) - { - return; - } - - // Tell the dx engine that our window is now the new size. - THROW_IF_FAILED(_renderEngine->SetWindowSize(size)); - - // Invalidate everything - _renderer->TriggerRedrawAll(); - - // Convert our new dimensions to characters - const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, - { static_cast(size.cx), static_cast(size.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 - const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() }); - if (SUCCEEDED(hr) && hr != S_FALSE) - { - _connection.Resize(vp.Height(), vp.Width()); - } - } - - void TermControl::_TerminalTitleChanged(const std::wstring_view& wstr) - { - _titleChangedHandlers(winrt::hstring{ wstr }); - } - - // Method Description: - // - Update the postion and size of the scrollbar to match the given - // viewport top, viewport height, and buffer size. - // The change will be actually handled in _ScrollbarChangeHandler. - // This should be done on the UI thread. Make sure the caller is calling - // us in a RunAsync block. - // Arguments: - // - viewTop: the top of the visible viewport, in rows. 0 indicates the top - // of the buffer. - // - viewHeight: the height of the viewport in rows. - // - bufferSize: the length of the buffer, in rows - void TermControl::_ScrollbarUpdater(Controls::Primitives::ScrollBar scrollBar, - const int viewTop, - const int viewHeight, - const int bufferSize) - { - const auto hiddenContent = bufferSize - viewHeight; - scrollBar.Maximum(hiddenContent); - scrollBar.Minimum(0); - scrollBar.ViewportSize(viewHeight); - - scrollBar.Value(viewTop); - } - - // Method Description: - // - Update the postion and size of the scrollbar to match the given - // viewport top, viewport height, and buffer size. - // Additionally fires a ScrollPositionChanged event for anyone who's - // registered an event handler for us. - // Arguments: - // - viewTop: the top of the visible viewport, in rows. 0 indicates the top - // of the buffer. - // - viewHeight: the height of the viewport in rows. - // - bufferSize: the length of the buffer, in rows - void TermControl::_TerminalScrollPositionChanged(const int viewTop, - const int viewHeight, - const int bufferSize) - { - // Since this callback fires from non-UI thread, we might be already - // closed/closing. - if (_closing.load()) - { - return; - } - - // Update our scrollbar - _scrollBar.Dispatcher().RunAsync(CoreDispatcherPriority::Low, [=]() { - // Even if we weren't closed/closing few lines above, we might be - // while waiting for this block of code to be dispatched. - if (_closing.load()) - { - return; - } - - _ScrollbarUpdater(_scrollBar, viewTop, viewHeight, bufferSize); - }); - - // Set this value as our next expected scroll position. - _lastScrollOffset = { viewTop }; - _scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize); - } - - hstring TermControl::Title() - { - if (!_initializedTerminal) - return L""; - - hstring hstr(_terminal->GetConsoleTitle()); - return hstr; - } - - // Method Description: - // - Given a copy-able selection, get the selected text from the buffer and send it to the - // Windows Clipboard (CascadiaWin32:main.cpp). - // - CopyOnSelect does NOT clear the selection - // Arguments: - // - trimTrailingWhitespace: enable removing any whitespace from copied selection - // and get text to appear on separate lines. - bool TermControl::CopySelectionToClipboard(bool trimTrailingWhitespace) - { - // no selection --> nothing to copy - if (_terminal == nullptr || !_terminal->IsSelectionActive()) - { - return false; - } - // extract text from buffer - const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace); - - // convert text: vector --> string - std::wstring textData; - for (const auto& text : bufferData.text) - { - textData += text; - } - - // convert text to HTML format - const auto htmlData = TextBuffer::GenHTML(bufferData, - _actualFont.GetUnscaledSize().Y, - _actualFont.GetFaceName(), - _settings.DefaultBackground(), - "Windows Terminal"); - - // convert to RTF format - const auto rtfData = TextBuffer::GenRTF(bufferData, - _actualFont.GetUnscaledSize().Y, - _actualFont.GetFaceName(), - _settings.DefaultBackground()); - - if (!_terminal->IsCopyOnSelectActive()) - { - _terminal->ClearSelection(); - } - - // send data up for clipboard - auto copyArgs = winrt::make_self(winrt::hstring(textData.data(), gsl::narrow(textData.size())), - winrt::to_hstring(htmlData), - winrt::to_hstring(rtfData)); - _clipboardCopyHandlers(*this, *copyArgs); - return true; - } - - // Method Description: - // - Initiate a paste operation. - void TermControl::PasteTextFromClipboard() - { - // attach TermControl::_SendInputToConnection() as the clipboardDataHandler. - // This is called when the clipboard data is loaded. - auto clipboardDataHandler = std::bind(&TermControl::_SendPastedTextToConnection, this, std::placeholders::_1); - auto pasteArgs = winrt::make_self(clipboardDataHandler); - - // send paste event up to TermApp - _clipboardPasteHandlers(*this, *pasteArgs); - } - - void TermControl::Close() - { - if (!_closing.exchange(true)) - { - // Stop accepting new output and state changes before we disconnect everything. - _connection.TerminalOutput(_connectionOutputEventToken); - _connectionStateChangedRevoker.revoke(); - - // Clear out the cursor timer, so it doesn't trigger again on us once we're destructed. - if (auto localCursorTimer{ std::exchange(_cursorTimer, std::nullopt) }) - { - localCursorTimer->Stop(); - // cursorTimer timer, now stopped, is destroyed. - } - - if (auto localAutoScrollTimer{ std::exchange(_autoScrollTimer, nullptr) }) - { - localAutoScrollTimer.Stop(); - // _autoScrollTimer timer, now stopped, is destroyed. - } - - if (auto localConnection{ std::exchange(_connection, nullptr) }) - { - localConnection.Close(); - // connection is destroyed. - } - - if (auto localRenderEngine{ std::exchange(_renderEngine, nullptr) }) - { - if (auto localRenderer{ std::exchange(_renderer, nullptr) }) - { - localRenderer->TriggerTeardown(); - // renderer is destroyed - } - // renderEngine is destroyed - } - - if (auto localTerminal{ std::exchange(_terminal, nullptr) }) - { - _initializedTerminal = false; - // terminal is destroyed. - } - } - } - - void TermControl::ScrollViewport(int viewTop) - { - _terminal->UserScrollViewport(viewTop); - } - - // Method Description: - // - Scrolls the viewport of the terminal and updates the scroll bar accordingly - // Arguments: - // - viewTop: the viewTop to scroll to - // The difference between this function and ScrollViewport is that this one also - // updates the _scrollBar after the viewport scroll. The reason _scrollBar is not updated in - // ScrollViewport is because ScrollViewport is being called by _ScrollbarChangeHandler - void TermControl::KeyboardScrollViewport(int viewTop) - { - _terminal->UserScrollViewport(viewTop); - _lastScrollOffset = std::nullopt; - _scrollBar.Value(static_cast(viewTop)); - } - - int TermControl::GetScrollOffset() - { - return _terminal->GetScrollOffset(); - } - - // Function Description: - // - Gets the height of the terminal in lines of text - // Return Value: - // - The height of the terminal in lines of text - int TermControl::GetViewHeight() const - { - const auto viewPort = _terminal->GetViewport(); - return viewPort.Height(); - } - - // Function Description: - // - Determines how much space (in pixels) an app would need to reserve to - // create a control with the settings stored in the settings param. This - // accounts for things like the font size and face, the initialRows and - // initialCols, and scrollbar visibility. The returned sized is based upon - // the provided DPI value - // Arguments: - // - settings: A IControlSettings with the settings to get the pixel size of. - // - dpi: The DPI we should create the terminal at. This affects things such - // as font size, scrollbar and other control scaling, etc. Make sure the - // caller knows what monitor the control is about to appear on. - // Return Value: - // - a point containing the requested dimensions in pixels. - winrt::Windows::Foundation::Point TermControl::GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi) - { - // Initialize our font information. - const auto* fontFace = settings.FontFace().c_str(); - const short fontHeight = gsl::narrow(settings.FontSize()); - // The font width doesn't terribly matter, we'll only be using the - // height to look it up - // The other params here also largely don't matter. - // The family is only used to determine if the font is truetype or - // not, but DX doesn't use that info at all. - // The Codepage is additionally not actually used by the DX engine at all. - FontInfo actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false }; - FontInfoDesired desiredFont = { actualFont }; - - // If the settings have negative or zero row or column counts, ignore those counts. - // (The lower TerminalCore layer also has upper bounds as well, but at this layer - // we may eventually impose different ones depending on how many pixels we can address.) - const auto cols = std::max(settings.InitialCols(), 1); - const auto rows = std::max(settings.InitialRows(), 1); - - // Create a DX engine and initialize it with our font and DPI. We'll - // then use it to measure how much space the requested rows and columns - // will take up. - // TODO: MSFT:21254947 - use a static function to do this instead of - // instantiating a DxEngine - auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); - THROW_IF_FAILED(dxEngine->UpdateDpi(dpi)); - THROW_IF_FAILED(dxEngine->UpdateFont(desiredFont, actualFont)); - - const float scale = dxEngine->GetScaling(); - const auto fontSize = actualFont.GetSize(); - - // Manually multiply by the scaling factor. The DX engine doesn't - // actually store the scaled font size in the fontInfo.GetSize() - // property when the DX engine is in Composition mode (which it is for - // the Terminal). At runtime, this is fine, as we'll transform - // everything by our scaling, so it'll work out. However, right now we - // need to get the exact pixel count. - const float fFontWidth = gsl::narrow(fontSize.X * scale); - const float fFontHeight = gsl::narrow(fontSize.Y * scale); - - // UWP XAML scrollbars aren't guaranteed to be the same size as the - // ComCtl scrollbars, but it's certainly close enough. - const auto scrollbarSize = GetSystemMetricsForDpi(SM_CXVSCROLL, dpi); - - double width = cols * fFontWidth; - - // Reserve additional space if scrollbar is intended to be visible - if (settings.ScrollState() == ScrollbarState::Visible) - { - width += scrollbarSize; - } - - double height = rows * fFontHeight; - auto thickness = _ParseThicknessFromPadding(settings.Padding()); - width += thickness.Left + thickness.Right; - height += thickness.Top + thickness.Bottom; - - return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; - } - - // Method Description: - // - Get the size of a single character of this control. The size is in - // DIPs. If you need it in _pixels_, you'll need to multiply by the - // current display scaling. - // Arguments: - // - - // Return Value: - // - The dimensions of a single character of this control, in DIPs - winrt::Windows::Foundation::Size TermControl::CharacterDimensions() const - { - const auto fontSize = _actualFont.GetSize(); - return { gsl::narrow_cast(fontSize.X), gsl::narrow_cast(fontSize.Y) }; - } - - // Method Description: - // - Get the absolute minimum size that this control can be resized to and - // still have 1x1 character visible. This includes the space needed for - // the scrollbar and the padding. - // Arguments: - // - - // Return Value: - // - The minimum size that this terminal control can be resized to and still - // have a visible character. - winrt::Windows::Foundation::Size TermControl::MinimumSize() const - { - const auto fontSize = _actualFont.GetSize(); - double width = fontSize.X; - double height = fontSize.Y; - // Reserve additional space if scrollbar is intended to be visible - if (_settings.ScrollState() == ScrollbarState::Visible) - { - width += _scrollBar.ActualWidth(); - } - - // Account for the size of any padding - auto thickness = _ParseThicknessFromPadding(_settings.Padding()); - width += thickness.Left + thickness.Right; - height += thickness.Top + thickness.Bottom; - - return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; - } - - // Method Description: - // - Create XAML Thickness object based on padding props provided. - // Used for controlling the TermControl XAML Grid container's Padding prop. - // Arguments: - // - padding: 2D padding values - // Single Double value provides uniform padding - // Two Double values provide isometric horizontal & vertical padding - // Four Double values provide independent padding for 4 sides of the bounding rectangle - // Return Value: - // - Windows::UI::Xaml::Thickness object - Windows::UI::Xaml::Thickness TermControl::_ParseThicknessFromPadding(const hstring padding) - { - const wchar_t singleCharDelim = L','; - std::wstringstream tokenStream(padding.c_str()); - std::wstring token; - uint8_t paddingPropIndex = 0; - std::array thicknessArr = {}; - size_t* idx = nullptr; - - // Get padding values till we run out of delimiter separated values in the stream - // or we hit max number of allowable values (= 4) for the bounding rectangle - // Non-numeral values detected will default to 0 - // std::getline will not throw exception unless flags are set on the wstringstream - // std::stod will throw invalid_argument expection if the input is an invalid double value - // std::stod will throw out_of_range expection if the input value is more than DBL_MAX - try - { - for (; std::getline(tokenStream, token, singleCharDelim) && (paddingPropIndex < thicknessArr.size()); paddingPropIndex++) - { - // std::stod internall calls wcstod which handles whitespace prefix (which is ignored) - // & stops the scan when first char outside the range of radix is encountered - // We'll be permissive till the extent that stod function allows us to be by default - // Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail - thicknessArr[paddingPropIndex] = std::stod(token, idx); - } - } - catch (...) - { - // If something goes wrong, even if due to a single bad padding value, we'll reset the index & return default 0 padding - paddingPropIndex = 0; - LOG_CAUGHT_EXCEPTION(); - } - - switch (paddingPropIndex) - { - case 1: - return ThicknessHelper::FromUniformLength(thicknessArr[0]); - case 2: - return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[0], thicknessArr[1]); - // No case for paddingPropIndex = 3, since it's not a norm to provide just Left, Top & Right padding values leaving out Bottom - case 4: - return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[2], thicknessArr[3]); - default: - return Thickness(); - } - } - - // Method Description: - // - Get the modifier keys that are currently pressed. This can be used to - // find out which modifiers (ctrl, alt, shift) are pressed in events that - // don't necessarily include that state. - // Return Value: - // - The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. - ControlKeyStates TermControl::_GetPressedModifierKeys() const - { - CoreWindow window = CoreWindow::GetForCurrentThread(); - // DONT USE - // != CoreVirtualKeyStates::None - // OR - // == CoreVirtualKeyStates::Down - // Sometimes with the key down, the state is Down | Locked. - // Sometimes with the key up, the state is Locked. - // IsFlagSet(Down) is the only correct solution. - - struct KeyModifier - { - VirtualKey vkey; - ControlKeyStates flags; - }; - - constexpr std::array modifiers{ { - { VirtualKey::RightMenu, ControlKeyStates::RightAltPressed }, - { VirtualKey::LeftMenu, ControlKeyStates::LeftAltPressed }, - { VirtualKey::RightControl, ControlKeyStates::RightCtrlPressed }, - { VirtualKey::LeftControl, ControlKeyStates::LeftCtrlPressed }, - { VirtualKey::Shift, ControlKeyStates::ShiftPressed }, - } }; - - ControlKeyStates flags; - - for (const auto& mod : modifiers) - { - const auto state = window.GetKeyState(mod.vkey); - const auto isDown = WI_IsFlagSet(state, CoreVirtualKeyStates::Down); - - if (isDown) - { - flags |= mod.flags; - } - } - - return flags; - } - - // Method Description: - // - Gets the corresponding viewport terminal position for the cursor - // by excluding the padding and normalizing with the font size. - // This is used for selection. - // Arguments: - // - cursorPosition: the (x,y) position of a given cursor (i.e.: mouse cursor). - // NOTE: origin (0,0) is top-left. - // Return Value: - // - the corresponding viewport terminal position for the given Point parameter - const COORD TermControl::_GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition) - { - // Exclude padding from cursor position calculation - COORD terminalPosition = { - static_cast(cursorPosition.X - _swapChainPanel.Margin().Left), - static_cast(cursorPosition.Y - _swapChainPanel.Margin().Top) - }; - - const auto fontSize = _actualFont.GetSize(); - FAIL_FAST_IF(fontSize.X == 0); - FAIL_FAST_IF(fontSize.Y == 0); - - // Normalize to terminal coordinates by using font size - terminalPosition.X /= fontSize.X; - terminalPosition.Y /= fontSize.Y; - - return terminalPosition; - } - - // Method Description: - // - Composition Completion handler for the TSFInputControl that - // handles writing text out to TerminalConnection - // Arguments: - // - text: the text to write to TerminalConnection - // Return Value: - // - - void TermControl::_CompositionCompleted(winrt::hstring text) - { - _connection.WriteInput(text); - } - - // Method Description: - // - CurrentCursorPosition handler for the TSFInputControl that - // handles returning current cursor position. - // Arguments: - // - eventArgs: event for storing the current cursor position - // Return Value: - // - - void TermControl::_CurrentCursorPositionHandler(const IInspectable& /*sender*/, const CursorPositionEventArgs& eventArgs) - { - const COORD cursorPos = _terminal->GetCursorPosition(); - Windows::Foundation::Point p = { gsl::narrow(cursorPos.X), gsl::narrow(cursorPos.Y) }; - eventArgs.CurrentPosition(p); - } - - // Method Description: - // - FontInfo handler for the TSFInputControl that - // handles returning current font information - // Arguments: - // - eventArgs: event for storing the current font information - // Return Value: - // - - void TermControl::_FontInfoHandler(const IInspectable& /*sender*/, const FontInfoEventArgs& eventArgs) - { - eventArgs.FontSize(CharacterDimensions()); - eventArgs.FontFace(_actualFont.GetFaceName()); - } - - // Method Description: - // - Returns the number of clicks that occurred (double and triple click support) - // Arguments: - // - clickPos: the (x,y) position of a given cursor (i.e.: mouse cursor). - // NOTE: origin (0,0) is top-left. - // - clickTime: the timestamp that the click occurred - // Return Value: - // - if the click is in the same position as the last click and within the timeout, the number of clicks within that time window - // - otherwise, 1 - const unsigned int TermControl::_NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime) - { - // if click occurred at a different location or past the multiClickTimer... - Timestamp delta; - THROW_IF_FAILED(UInt64Sub(clickTime, _lastMouseClick, &delta)); - if (clickPos != _lastMouseClickPos || delta > _multiClickTimer) - { - // exit early. This is a single click. - _multiClickCounter = 1; - } - else - { - _multiClickCounter++; - } - return _multiClickCounter; - } - - // Method Description: - // - Calculates speed of single axis of auto scrolling. It has to allow for both - // fast and precise selection. - // Arguments: - // - cursorDistanceFromBorder: distance from viewport border to cursor, in pixels. Must be non-negative. - // Return Value: - // - positive speed in characters / sec - double TermControl::_GetAutoScrollSpeed(double cursorDistanceFromBorder) const - { - // The numbers below just feel well, feel free to change. - // TODO: Maybe account for space beyond border that user has available - return std::pow(cursorDistanceFromBorder, 2.0) / 25.0 + 2.0; - } - - // -------------------------------- WinRT Events --------------------------------- - // Winrt events need a method for adding a callback to the event and removing the callback. - // These macros will define them both for you. - DEFINE_EVENT(TermControl, TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs); - DEFINE_EVENT(TermControl, ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs); - - DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, PasteFromClipboard, _clipboardPasteHandlers, TerminalControl::TermControl, TerminalControl::PasteFromClipboardEventArgs); - DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, CopyToClipboard, _clipboardCopyHandlers, TerminalControl::TermControl, TerminalControl::CopyToClipboardEventArgs); - // clang-format on -} +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "TermControl.h" +#include +#include +#include +#include +#include +#include +#include "..\..\types\inc\GlyphWidth.hpp" + +#include "TermControl.g.cpp" +#include "TermControlAutomationPeer.h" + +using namespace ::Microsoft::Console::Types; +using namespace ::Microsoft::Terminal::Core; +using namespace winrt::Windows::UI::Xaml; +using namespace winrt::Windows::UI::Core; +using namespace winrt::Windows::System; +using namespace winrt::Microsoft::Terminal::Settings; + +namespace winrt::Microsoft::Terminal::TerminalControl::implementation +{ + // Helper static function to ensure that all ambiguous-width glyphs are reported as narrow. + // See microsoft/terminal#2066 for more info. + static bool _IsGlyphWideForceNarrowFallback(const std::wstring_view /* glyph */) + { + return false; // glyph is not wide. + } + + static bool _EnsureStaticInitialization() + { + // use C++11 magic statics to make sure we only do this once. + static bool initialized = []() { + // *** THIS IS A SINGLETON *** + SetGlyphWidthFallback(_IsGlyphWideForceNarrowFallback); + + return true; + }(); + return initialized; + } + + TermControl::TermControl() : + TermControl(Settings::TerminalSettings{}, TerminalConnection::ITerminalConnection{ nullptr }) + { + } + + TermControl::TermControl(Settings::IControlSettings settings, TerminalConnection::ITerminalConnection connection) : + _connection{ connection }, + _initializedTerminal{ false }, + _root{ nullptr }, + _swapChainPanel{ nullptr }, + _settings{ settings }, + _closing{ false }, + _lastScrollOffset{ std::nullopt }, + _autoScrollVelocity{ 0 }, + _autoScrollingPointerPoint{ std::nullopt }, + _autoScrollTimer{}, + _lastAutoScrollUpdateTime{ std::nullopt }, + _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 }, + _cursorTimer{}, + _lastMouseClick{}, + _lastMouseClickPos{}, + _tsfInputControl{ nullptr } + { + _EnsureStaticInitialization(); + _Create(); + } + + void TermControl::_Create() + { + Controls::Grid container; + + Controls::ColumnDefinition contentColumn{}; + Controls::ColumnDefinition scrollbarColumn{}; + contentColumn.Width(GridLength{ 1.0, GridUnitType::Star }); + scrollbarColumn.Width(GridLength{ 1.0, GridUnitType::Auto }); + + container.ColumnDefinitions().Append(contentColumn); + container.ColumnDefinitions().Append(scrollbarColumn); + + _scrollBar = Controls::Primitives::ScrollBar{}; + _scrollBar.Orientation(Controls::Orientation::Vertical); + _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); + _scrollBar.HorizontalAlignment(HorizontalAlignment::Right); + _scrollBar.VerticalAlignment(VerticalAlignment::Stretch); + + // Initialize the scrollbar with some placeholder values. + // The scrollbar will be updated with real values on _Initialize + _scrollBar.Maximum(1); + _scrollBar.ViewportSize(10); + _scrollBar.IsTabStop(false); + _scrollBar.SmallChange(1); + _scrollBar.LargeChange(4); + _scrollBar.Visibility(Visibility::Visible); + + _tsfInputControl = TSFInputControl(); + _tsfInputControl.CompositionCompleted({ this, &TermControl::_CompositionCompleted }); + _tsfInputControl.CurrentCursorPosition({ this, &TermControl::_CurrentCursorPositionHandler }); + _tsfInputControl.CurrentFontInfo({ this, &TermControl::_FontInfoHandler }); + container.Children().Append(_tsfInputControl); + + // Create the SwapChainPanel that will display our content + Controls::SwapChainPanel swapChainPanel; + + _sizeChangedRevoker = swapChainPanel.SizeChanged(winrt::auto_revoke, { this, &TermControl::_SwapChainSizeChanged }); + _compositionScaleChangedRevoker = swapChainPanel.CompositionScaleChanged(winrt::auto_revoke, { this, &TermControl::_SwapChainScaleChanged }); + + // Initialize the terminal only once the swapchainpanel is loaded - that + // way, we'll be able to query the real pixel size it got on layout + _layoutUpdatedRevoker = swapChainPanel.LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { + // This event fires every time the layout changes, but it is always the last one to fire + // in any layout change chain. That gives us great flexibility in finding the right point + // at which to initialize our renderer (and our terminal). + // Any earlier than the last layout update and we may not know the terminal's starting size. + if (_InitializeTerminal()) + { + // Only let this succeed once. + this->_layoutUpdatedRevoker.revoke(); + } + }); + + container.Children().Append(swapChainPanel); + container.Children().Append(_scrollBar); + Controls::Grid::SetColumn(swapChainPanel, 0); + Controls::Grid::SetColumn(_scrollBar, 1); + + Controls::Grid root{}; + Controls::Image bgImageLayer{}; + root.Children().Append(bgImageLayer); + root.Children().Append(container); + + _root = root; + _bgImageLayer = bgImageLayer; + + _swapChainPanel = swapChainPanel; + this->Content(_root); + + _ApplyUISettings(); + + // These are important: + // 1. When we get tapped, focus us + this->Tapped([this](auto&, auto& e) { + this->Focus(FocusState::Pointer); + e.Handled(true); + }); + // 2. Make sure we can be focused (why this isn't `Focusable` I'll never know) + this->IsTabStop(true); + // 3. Actually not sure about this one. Maybe it isn't necessary either. + this->AllowFocusOnInteraction(true); + + // DON'T CALL _InitializeTerminal here - wait until the swap chain is loaded to do that. + + // Subscribe to the connection's disconnected event and call our connection closed handlers. + _connectionStateChangedRevoker = _connection.StateChanged(winrt::auto_revoke, [weakThis = get_weak()](auto&& /*s*/, auto&& /*v*/) { + if (auto strongThis{ weakThis.get() }) + { + strongThis->_ConnectionStateChangedHandlers(*strongThis, nullptr); + } + }); + } + + // Method Description: + // - Given new settings for this profile, applies the settings to the current terminal. + // Arguments: + // - newSettings: New settings values for the profile in this terminal. + // Return Value: + // - + void TermControl::UpdateSettings(Settings::IControlSettings newSettings) + { + _settings = newSettings; + + // Dispatch a call to the UI thread to apply the new settings to the + // terminal. + _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this]() { + // Update our control settings + _ApplyUISettings(); + + // Update DxEngine's SelectionBackground + _renderEngine->SetSelectionBackground(_settings.SelectionBackground()); + + // Update the terminal core with its new Core settings + _terminal->UpdateSettings(_settings); + + // Refresh our font with the renderer + _UpdateFont(); + + const auto width = _swapChainPanel.ActualWidth(); + const auto height = _swapChainPanel.ActualHeight(); + if (width != 0 && height != 0) + { + // If the font size changed, or the _swapchainPanel's size changed + // for any reason, we'll need to make sure to also resize the + // buffer. _DoResize will invalidate everything for us. + auto lock = _terminal->LockForWriting(); + _DoResize(width, height); + } + + // set TSF Foreground + Media::SolidColorBrush foregroundBrush{}; + foregroundBrush.Color(ColorRefToColor(_settings.DefaultForeground())); + _tsfInputControl.Foreground(foregroundBrush); + }); + } + + // Method Description: + // - Style our UI elements based on the values in our _settings, and set up + // other control-specific settings. This method will be called whenever + // the settings are reloaded. + // * Calls _InitializeBackgroundBrush to set up the Xaml brush responsible + // for the control's background + // * Calls _BackgroundColorChanged to style the background of the control + // - Core settings will be passed to the terminal in _InitializeTerminal + // Arguments: + // - + // Return Value: + // - + void TermControl::_ApplyUISettings() + { + _InitializeBackgroundBrush(); + + uint32_t bg = _settings.DefaultBackground(); + _BackgroundColorChanged(bg); + + // Apply padding as swapChainPanel's margin + auto newMargin = _ParseThicknessFromPadding(_settings.Padding()); + auto existingMargin = _swapChainPanel.Margin(); + _swapChainPanel.Margin(newMargin); + + if (newMargin != existingMargin && newMargin != Thickness{ 0 }) + { + TraceLoggingWrite(g_hTerminalControlProvider, + "NonzeroPaddingApplied", + TraceLoggingDescription("An event emitted when a control has padding applied to it"), + TraceLoggingStruct(4, "Padding"), + TraceLoggingFloat64(newMargin.Left, "Left"), + TraceLoggingFloat64(newMargin.Top, "Top"), + TraceLoggingFloat64(newMargin.Right, "Right"), + TraceLoggingFloat64(newMargin.Bottom, "Bottom"), + TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES), + TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance)); + } + + // Initialize our font information. + const auto* fontFace = _settings.FontFace().c_str(); + const short fontHeight = gsl::narrow(_settings.FontSize()); + // The font width doesn't terribly matter, we'll only be using the + // height to look it up + // The other params here also largely don't matter. + // The family is only used to determine if the font is truetype or + // not, but DX doesn't use that info at all. + // The Codepage is additionally not actually used by the DX engine at all. + _actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false }; + _desiredFont = { _actualFont }; + + // set TSF Foreground + Media::SolidColorBrush foregroundBrush{}; + foregroundBrush.Color(ColorRefToColor(_settings.DefaultForeground())); + _tsfInputControl.Foreground(foregroundBrush); + _tsfInputControl.Margin(newMargin); + + // set number of rows to scroll at a time + _rowsToScroll = _settings.RowsToScroll(); + } + + // Method Description: + // - Set up each layer's brush used to display the control's background. + // - Respects the settings for acrylic, background image and opacity from + // _settings. + // * If acrylic is not enabled, setup a solid color background, otherwise + // use bgcolor as acrylic's tint + // - Avoids image flickering and acrylic brush redraw if settings are changed + // but the appropriate brush is still in place. + // - Does not apply background color outside of acrylic mode; + // _BackgroundColorChanged must be called to do so. + // Arguments: + // - + // Return Value: + // - + void TermControl::_InitializeBackgroundBrush() + { + if (_settings.UseAcrylic()) + { + // See if we've already got an acrylic background brush + // to avoid the flicker when setting up a new one + auto acrylic = _root.Background().try_as(); + + // Instantiate a brush if there's not already one there + if (acrylic == nullptr) + { + acrylic = Media::AcrylicBrush{}; + acrylic.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); + } + + // see GH#1082: Initialize background color so we don't get a + // fade/flash when _BackgroundColorChanged is called + uint32_t color = _settings.DefaultBackground(); + winrt::Windows::UI::Color bgColor{}; + bgColor.R = GetRValue(color); + bgColor.G = GetGValue(color); + bgColor.B = GetBValue(color); + bgColor.A = 255; + + acrylic.FallbackColor(bgColor); + acrylic.TintColor(bgColor); + + // Apply brush settings + acrylic.TintOpacity(_settings.TintOpacity()); + + // Apply brush to control if it's not already there + if (_root.Background() != acrylic) + { + _root.Background(acrylic); + } + } + else + { + Media::SolidColorBrush solidColor{}; + _root.Background(solidColor); + } + + if (!_settings.BackgroundImage().empty()) + { + Windows::Foundation::Uri imageUri{ _settings.BackgroundImage() }; + + // Check if the image brush is already pointing to the image + // in the modified settings; if it isn't (or isn't there), + // set a new image source for the brush + auto imageSource = _bgImageLayer.Source().try_as(); + + if (imageSource == nullptr || + imageSource.UriSource() == nullptr || + imageSource.UriSource().RawUri() != imageUri.RawUri()) + { + // Note that BitmapImage handles the image load asynchronously, + // which is especially important since the image + // may well be both large and somewhere out on the + // internet. + Media::Imaging::BitmapImage image(imageUri); + _bgImageLayer.Source(image); + } + + // Apply stretch, opacity and alignment settings + _bgImageLayer.Stretch(_settings.BackgroundImageStretchMode()); + _bgImageLayer.Opacity(_settings.BackgroundImageOpacity()); + _bgImageLayer.HorizontalAlignment(_settings.BackgroundImageHorizontalAlignment()); + _bgImageLayer.VerticalAlignment(_settings.BackgroundImageVerticalAlignment()); + } + else + { + _bgImageLayer.Source(nullptr); + } + } + + // Method Description: + // - Style the background of the control with the provided background color + // Arguments: + // - color: The background color to use as a uint32 (aka DWORD COLORREF) + // Return Value: + // - + void TermControl::_BackgroundColorChanged(const uint32_t color) + { + _root.Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this, color]() { + const auto R = GetRValue(color); + const auto G = GetGValue(color); + const auto B = GetBValue(color); + + winrt::Windows::UI::Color bgColor{}; + bgColor.R = R; + bgColor.G = G; + bgColor.B = B; + bgColor.A = 255; + + if (auto acrylic = _root.Background().try_as()) + { + acrylic.FallbackColor(bgColor); + acrylic.TintColor(bgColor); + } + else if (auto solidColor = _root.Background().try_as()) + { + solidColor.Color(bgColor); + } + + // Set the default background as transparent to prevent the + // DX layer from overwriting the background image or acrylic effect + _settings.DefaultBackground(ARGB(0, R, G, B)); + }); + } + + TermControl::~TermControl() + { + Close(); + } + + Windows::UI::Xaml::Automation::Peers::AutomationPeer TermControl::OnCreateAutomationPeer() + { + Windows::UI::Xaml::Automation::Peers::AutomationPeer autoPeer{ nullptr }; + if (GetUiaData()) + { + try + { + // create a custom automation peer with this code pattern: + // (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers) + autoPeer = winrt::make(this); + } + CATCH_LOG(); + } + return autoPeer; + } + + ::Microsoft::Console::Types::IUiaData* TermControl::GetUiaData() const + { + return _terminal.get(); + } + + const FontInfo TermControl::GetActualFont() const + { + return _actualFont; + } + + const Windows::UI::Xaml::Thickness TermControl::GetPadding() const + { + return _swapChainPanel.Margin(); + } + + TerminalConnection::ConnectionState TermControl::ConnectionState() const + { + return _connection.State(); + } + + void TermControl::SwapChainChanged() + { + if (!_initializedTerminal) + { + return; + } + + auto chain = _renderEngine->GetSwapChain(); + _swapChainPanel.Dispatcher().RunAsync(CoreDispatcherPriority::High, [=]() { + auto lock = _terminal->LockForWriting(); + auto nativePanel = _swapChainPanel.as(); + nativePanel->SetSwapChain(chain.Get()); + }); + } + + bool TermControl::_InitializeTerminal() + { + if (_initializedTerminal) + { + return false; + } + + const auto windowWidth = _swapChainPanel.ActualWidth(); // Width() and Height() are NaN? + const auto windowHeight = _swapChainPanel.ActualHeight(); + + if (windowWidth == 0 || windowHeight == 0) + { + return false; + } + + _terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>(); + + // First create the render thread. + // Then stash a local pointer to the render thread so we can initialize it and enable it + // to paint itself *after* we hand off its ownership to the renderer. + // We split up construction and initialization of the render thread object this way + // because the renderer and render thread have circular references to each other. + auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>(); + auto* const localPointerToThread = renderThread.get(); + + // Now create the renderer and initialize the render thread. + _renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread)); + ::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer; + + THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); + + // Set up the DX Engine + auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); + _renderer->AddRenderEngine(dxEngine.get()); + + // Initialize our font with the renderer + // We don't have to care about DPI. We'll get a change message immediately if it's not 96 + // and react accordingly. + _UpdateFont(); + + const COORD windowSize{ static_cast(windowWidth), static_cast(windowHeight) }; + + // Fist set up the dx engine with the window size in pixels. + // Then, using the font, get the number of characters that can fit. + // Resize our terminal connection to match that size, and initialize the terminal with that size. + const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize); + THROW_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); + + // Update DxEngine's SelectionBackground + dxEngine->SetSelectionBackground(_settings.SelectionBackground()); + + const auto vp = dxEngine->GetViewportInCharacters(viewInPixels); + const auto width = vp.Width(); + const auto height = vp.Height(); + _connection.Resize(height, width); + + // Override the default width and height to match the size of the swapChainPanel + _settings.InitialCols(width); + _settings.InitialRows(height); + + _terminal->CreateFromSettings(_settings, renderTarget); + + // Tell the DX Engine to notify us when the swap chain changes. + dxEngine->SetCallback(std::bind(&TermControl::SwapChainChanged, this)); + + THROW_IF_FAILED(dxEngine->Enable()); + _renderEngine = std::move(dxEngine); + + auto onRecieveOutputFn = [this](const hstring str) { + _terminal->Write(str.c_str()); + }; + _connectionOutputEventToken = _connection.TerminalOutput(onRecieveOutputFn); + + auto inputFn = std::bind(&TermControl::_SendInputToConnection, this, std::placeholders::_1); + _terminal->SetWriteInputCallback(inputFn); + + auto chain = _renderEngine->GetSwapChain(); + _swapChainPanel.Dispatcher().RunAsync(CoreDispatcherPriority::High, [this, chain]() { + _terminal->LockConsole(); + auto nativePanel = _swapChainPanel.as(); + nativePanel->SetSwapChain(chain.Get()); + _terminal->UnlockConsole(); + }); + + // Set up the height of the ScrollViewer and the grid we're using to fake our scrolling height + auto bottom = _terminal->GetViewport().BottomExclusive(); + auto bufferHeight = bottom; + + const auto originalMaximum = _scrollBar.Maximum(); + const auto originalMinimum = _scrollBar.Minimum(); + const auto originalValue = _scrollBar.Value(); + const auto originalViewportSize = _scrollBar.ViewportSize(); + + _scrollBar.Maximum(bufferHeight - bufferHeight); + _scrollBar.Minimum(0); + _scrollBar.Value(0); + _scrollBar.ViewportSize(bufferHeight); + _scrollBar.ValueChanged({ this, &TermControl::_ScrollbarChangeHandler }); + _scrollBar.PointerPressed({ this, &TermControl::_CapturePointer }); + _scrollBar.PointerReleased({ this, &TermControl::_ReleasePointerCapture }); + + // Apply settings for scrollbar + if (_settings.ScrollState() == ScrollbarState::Visible) + { + _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); + } + else if (_settings.ScrollState() == ScrollbarState::Hidden) + { + _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::None); + + // In the scenario where the user has turned off the OS setting to automatically hide scollbars, the + // Terminal scrollbar would still be visible; so, we need to set the control's visibility accordingly to + // achieve the intended effect. + _scrollBar.Visibility(Visibility::Collapsed); + } + else + { + // Default behavior + _scrollBar.IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); + } + + _root.PointerWheelChanged({ this, &TermControl::_MouseWheelHandler }); + + // These need to be hooked up to the SwapChainPanel because we don't want the scrollbar to respond to pointer events (GitHub #950) + _swapChainPanel.PointerPressed({ this, &TermControl::_PointerPressedHandler }); + _swapChainPanel.PointerMoved({ this, &TermControl::_PointerMovedHandler }); + _swapChainPanel.PointerReleased({ this, &TermControl::_PointerReleasedHandler }); + + localPointerToThread->EnablePainting(); + + // No matter what order these guys are in, The KeyDown's will fire + // before the CharacterRecieved, so we can't easily get characters + // first, then fallback to getting keys from vkeys. + // TODO: This apparently handles keys and characters correctly, though + // I'd keep an eye on it, and test more. + // I presume that the characters that aren't translated by terminalInput + // just end up getting ignored, and the rest of the input comes + // through CharacterRecieved. + // I don't believe there's a difference between KeyDown and + // PreviewKeyDown for our purposes + // These two handlers _must_ be on this, not _root. + this->PreviewKeyDown({ this, &TermControl::_KeyDownHandler }); + this->CharacterReceived({ this, &TermControl::_CharacterHandler }); + + auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1); + _terminal->SetTitleChangedCallback(pfnTitleChanged); + + auto pfnBackgroundColorChanged = std::bind(&TermControl::_BackgroundColorChanged, this, std::placeholders::_1); + _terminal->SetBackgroundCallback(pfnBackgroundColorChanged); + + auto pfnScrollPositionChanged = std::bind(&TermControl::_TerminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + _terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged); + + static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast(1.0 / 30.0 * 1000000)); + _autoScrollTimer.Interval(AutoScrollUpdateInterval); + _autoScrollTimer.Tick({ this, &TermControl::_UpdateAutoScroll }); + + // Set up blinking cursor + int blinkTime = GetCaretBlinkTime(); + if (blinkTime != INFINITE) + { + // Create a timer + _cursorTimer = std::make_optional(DispatcherTimer()); + _cursorTimer.value().Interval(std::chrono::milliseconds(blinkTime)); + _cursorTimer.value().Tick({ this, &TermControl::_BlinkCursor }); + _cursorTimer.value().Start(); + } + else + { + // The user has disabled cursor blinking + _cursorTimer = std::nullopt; + } + + // import value from WinUser (convert from milli-seconds to micro-seconds) + _multiClickTimer = GetDoubleClickTime() * 1000; + + _gotFocusRevoker = this->GotFocus(winrt::auto_revoke, { this, &TermControl::_GotFocusHandler }); + _lostFocusRevoker = this->LostFocus(winrt::auto_revoke, { this, &TermControl::_LostFocusHandler }); + + // Focus the control here. If we do it up above (in _Create_), then the + // focus won't actually get passed to us. I believe this is because + // we're not technically a part of the UI tree yet, so focusing us + // becomes a no-op. + this->Focus(FocusState::Programmatic); + + _connection.Start(); + _initializedTerminal = true; + return true; + } + + void TermControl::_CharacterHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, + Input::CharacterReceivedRoutedEventArgs const& e) + { + if (_closing) + { + return; + } + + const auto ch = e.Character(); + + const bool handled = _terminal->SendCharEvent(ch); + e.Handled(handled); + } + + void TermControl::_KeyDownHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, + Input::KeyRoutedEventArgs const& e) + { + // mark event as handled and do nothing if... + // - closing + // - key modifier is pressed + // NOTE: for key combos like CTRL + C, two events are fired (one for CTRL, one for 'C'). We care about the 'C' event and then check for key modifiers below. + if (_closing || + e.OriginalKey() == VirtualKey::Control || + e.OriginalKey() == VirtualKey::Shift || + e.OriginalKey() == VirtualKey::Menu || + e.OriginalKey() == VirtualKey::LeftWindows || + e.OriginalKey() == VirtualKey::RightWindows) + + { + e.Handled(true); + return; + } + + const auto modifiers = _GetPressedModifierKeys(); + const auto vkey = gsl::narrow_cast(e.OriginalKey()); + const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); + bool handled = false; + + // GH#2235: Terminal::Settings hasn't been modified to differentiate between AltGr and Ctrl+Alt yet. + // -> Don't check for key bindings if this is an AltGr key combination. + if (!modifiers.IsAltGrPressed()) + { + auto bindings = _settings.KeyBindings(); + if (bindings) + { + handled = bindings.TryKeyChord({ + modifiers.IsCtrlPressed(), + modifiers.IsAltPressed(), + modifiers.IsShiftPressed(), + vkey, + }); + } + } + + if (!handled) + { + handled = _TrySendKeyEvent(vkey, scanCode, modifiers); + } + + // Manually prevent keyboard navigation with tab. We want to send tab to + // the terminal, and we don't want to be able to escape focus of the + // control with tab. + if (e.OriginalKey() == VirtualKey::Tab) + { + handled = true; + } + + e.Handled(handled); + } + + // Method Description: + // - Send this particular key event to the terminal. + // See Terminal::SendKeyEvent for more information. + // - Clears the current selection. + // - Makes the cursor briefly visible during typing. + // Arguments: + // - vkey: The vkey of the key pressed. + // - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. + bool TermControl::_TrySendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates modifiers) + { + _terminal->ClearSelection(); + + // If the terminal translated the key, mark the event as handled. + // This will prevent the system from trying to get the character out + // of it and sending us a CharacterRecieved event. + const auto handled = vkey ? _terminal->SendKeyEvent(vkey, scanCode, modifiers) : true; + + if (_cursorTimer.has_value()) + { + // Manually show the cursor when a key is pressed. Restarting + // the timer prevents flickering. + _terminal->SetCursorVisible(true); + _cursorTimer.value().Start(); + } + + return handled; + } + + // Method Description: + // - handle a mouse click event. Begin selection process. + // Arguments: + // - sender: the XAML element responding to the pointer input + // - args: event data + void TermControl::_PointerPressedHandler(Windows::Foundation::IInspectable const& sender, + Input::PointerRoutedEventArgs const& args) + { + _CapturePointer(sender, args); + + const auto ptr = args.Pointer(); + const auto point = args.GetCurrentPoint(_root); + + if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) + { + // Ignore mouse events while the terminal does not have focus. + // This prevents the user from selecting and copying text if they + // click inside the current tab to refocus the terminal window. + if (!_focused) + { + args.Handled(true); + return; + } + + const auto modifiers = static_cast(args.KeyModifiers()); + // static_cast to a uint32_t because we can't use the WI_IsFlagSet + // macro directly with a VirtualKeyModifiers + const auto altEnabled = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Menu)); + const auto shiftEnabled = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Shift)); + + if (point.Properties().IsLeftButtonPressed()) + { + const auto cursorPosition = point.Position(); + const auto terminalPosition = _GetTerminalPosition(cursorPosition); + + // handle ALT key + _terminal->SetBoxSelection(altEnabled); + + auto clickCount = _NumberOfClicks(cursorPosition, point.Timestamp()); + + // 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->TripleClickSelection(terminalPosition); + _renderer->TriggerSelection(); + } + else if (multiClickMapper == 2) + { + _terminal->DoubleClickSelection(terminalPosition); + _renderer->TriggerSelection(); + } + else + { + // save location before rendering + _terminal->SetSelectionAnchor(terminalPosition); + + _renderer->TriggerSelection(); + _lastMouseClick = point.Timestamp(); + _lastMouseClickPos = cursorPosition; + } + } + else if (point.Properties().IsRightButtonPressed()) + { + // copyOnSelect causes right-click to always paste + if (_terminal->IsCopyOnSelectActive() || !_terminal->IsSelectionActive()) + { + PasteTextFromClipboard(); + } + else + { + CopySelectionToClipboard(!shiftEnabled); + } + } + } + else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) + { + const auto contactRect = point.Properties().ContactRect(); + // Set our touch rect, to start a pan. + _touchAnchor = winrt::Windows::Foundation::Point{ contactRect.X, contactRect.Y }; + } + + args.Handled(true); + } + + // Method Description: + // - handle a mouse moved event. Specifically handling mouse drag to update selection process. + // Arguments: + // - sender: not used + // - args: event data + void TermControl::_PointerMovedHandler(Windows::Foundation::IInspectable const& /*sender*/, + Input::PointerRoutedEventArgs const& args) + { + const auto ptr = args.Pointer(); + const auto point = args.GetCurrentPoint(_root); + + if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) + { + if (point.Properties().IsLeftButtonPressed()) + { + const auto cursorPosition = point.Position(); + _SetEndSelectionPointAtCursor(cursorPosition); + + const double cursorBelowBottomDist = cursorPosition.Y - _swapChainPanel.Margin().Top - _swapChainPanel.ActualHeight(); + const double cursorAboveTopDist = -1 * cursorPosition.Y + _swapChainPanel.Margin().Top; + + constexpr double MinAutoScrollDist = 2.0; // Arbitrary value + double newAutoScrollVelocity = 0.0; + if (cursorBelowBottomDist > MinAutoScrollDist) + { + newAutoScrollVelocity = _GetAutoScrollSpeed(cursorBelowBottomDist); + } + else if (cursorAboveTopDist > MinAutoScrollDist) + { + newAutoScrollVelocity = -1.0 * _GetAutoScrollSpeed(cursorAboveTopDist); + } + + if (newAutoScrollVelocity != 0) + { + _TryStartAutoScroll(point, newAutoScrollVelocity); + } + else + { + _TryStopAutoScroll(ptr.PointerId()); + } + } + } + else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch && _touchAnchor) + { + const auto contactRect = point.Properties().ContactRect(); + winrt::Windows::Foundation::Point newTouchPoint{ contactRect.X, contactRect.Y }; + const auto anchor = _touchAnchor.value(); + + // Get the difference between the point we've dragged to and the start of the touch. + const float fontHeight = float(_actualFont.GetSize().Y); + + const float dy = newTouchPoint.Y - anchor.Y; + + // If we've moved more than one row of text, we'll want to scroll the viewport + if (std::abs(dy) > fontHeight) + { + // Multiply by -1, because moving the touch point down will + // create a positive delta, but we want the viewport to move up, + // so we'll need a negative scroll amount (and the inverse for + // panning down) + const float numRows = -1.0f * (dy / fontHeight); + + const auto currentOffset = this->GetScrollOffset(); + const double newValue = (numRows) + (currentOffset); + + // Clear our expected scroll offset. The viewport will now move + // in response to our user input. + _lastScrollOffset = std::nullopt; + _scrollBar.Value(static_cast(newValue)); + + // Use this point as our new scroll anchor. + _touchAnchor = newTouchPoint; + } + } + args.Handled(true); + } + + // Method Description: + // - Event handler for the PointerReleased event. We use this to de-anchor + // touch events, to stop scrolling via touch. + // Arguments: + // - sender: the XAML element responding to the pointer input + // - args: event data + void TermControl::_PointerReleasedHandler(Windows::Foundation::IInspectable const& sender, + Input::PointerRoutedEventArgs const& args) + { + _ReleasePointerCapture(sender, args); + + const auto ptr = args.Pointer(); + + if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) + { + const auto modifiers = static_cast(args.KeyModifiers()); + // static_cast to a uint32_t because we can't use the WI_IsFlagSet + // macro directly with a VirtualKeyModifiers + const auto shiftEnabled = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Shift)); + + if (_terminal->IsCopyOnSelectActive()) + { + CopySelectionToClipboard(!shiftEnabled); + } + } + else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) + { + _touchAnchor = std::nullopt; + } + + _TryStopAutoScroll(ptr.PointerId()); + + args.Handled(true); + } + + // Method Description: + // - Event handler for the PointerWheelChanged event. This is raised in + // response to mouse wheel changes. Depending upon what modifier keys are + // pressed, different actions will take place. + // Arguments: + // - args: the event args containing information about t`he mouse wheel event. + void TermControl::_MouseWheelHandler(Windows::Foundation::IInspectable const& /*sender*/, + Input::PointerRoutedEventArgs const& args) + { + const auto point = args.GetCurrentPoint(_root); + const auto delta = point.Properties().MouseWheelDelta(); + + // Get the state of the Ctrl & Shift keys + // static_cast to a uint32_t because we can't use the WI_IsFlagSet macro + // directly with a VirtualKeyModifiers + const auto modifiers = static_cast(args.KeyModifiers()); + const auto ctrlPressed = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Control)); + const auto shiftPressed = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Shift)); + + if (ctrlPressed && shiftPressed) + { + _MouseTransparencyHandler(delta); + } + else if (ctrlPressed) + { + _MouseZoomHandler(delta); + } + else + { + _MouseScrollHandler(delta, point); + } + } + + // Method Description: + // - Adjust the opacity of the acrylic background in response to a mouse + // scrolling event. + // Arguments: + // - mouseDelta: the mouse wheel delta that triggered this event. + void TermControl::_MouseTransparencyHandler(const double mouseDelta) + { + // Transparency is on a scale of [0.0,1.0], so only increment by .01. + const auto effectiveDelta = mouseDelta < 0 ? -.01 : .01; + + if (_settings.UseAcrylic()) + { + try + { + auto acrylicBrush = _root.Background().as(); + acrylicBrush.TintOpacity(acrylicBrush.TintOpacity() + effectiveDelta); + } + CATCH_LOG(); + } + } + + // Method Description: + // - Adjust the font size of the terminal in response to a mouse scrolling + // event. + // Arguments: + // - mouseDelta: the mouse wheel delta that triggered this event. + void TermControl::_MouseZoomHandler(const double mouseDelta) + { + const auto fontDelta = mouseDelta < 0 ? -1 : 1; + AdjustFontSize(fontDelta); + } + + // Method Description: + // - Reset the font size of the terminal to its default size. + // Arguments: + // - none + void TermControl::ResetFontSize() + { + _SetFontSize(_settings.FontSize()); + } + + // Method Description: + // - Adjust the font size of the terminal control. + // Arguments: + // - fontSizeDelta: The amount to increase or decrease the font size by. + void TermControl::AdjustFontSize(int fontSizeDelta) + { + const auto newSize = _desiredFont.GetEngineSize().Y + fontSizeDelta; + _SetFontSize(newSize); + } + + // Method Description: + // - Scroll the visible viewport in response to a mouse wheel event. + // Arguments: + // - mouseDelta: the mouse wheel delta that triggered this event. + void TermControl::_MouseScrollHandler(const double mouseDelta, Windows::UI::Input::PointerPoint const& pointerPoint) + { + const auto currentOffset = this->GetScrollOffset(); + + // negative = down, positive = up + // However, for us, the signs are flipped. + const auto rowDelta = mouseDelta < 0 ? 1.0 : -1.0; + + // With one of the precision mouses, one click is always a multiple of 120, + // but the "smooth scrolling" mode results in non-int values + + // Number of rows to scroll is now populated from settings. + double newValue = (_rowsToScroll * rowDelta) + (currentOffset); + + // Clear our expected scroll offset. The viewport will now move in + // response to our user input. + _lastScrollOffset = std::nullopt; + // The scroll bar's ValueChanged handler will actually move the viewport + // for us. + _scrollBar.Value(static_cast(newValue)); + + if (_terminal->IsSelectionActive() && pointerPoint.Properties().IsLeftButtonPressed()) + { + // If user is mouse selecting and scrolls, they then point at new character. + // Make sure selection reflects that immediately. + _SetEndSelectionPointAtCursor(pointerPoint.Position()); + } + } + + void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/, + Controls::Primitives::RangeBaseValueChangedEventArgs const& args) + { + const auto newValue = args.NewValue(); + + // If we've stored a lastScrollOffset, that means the terminal has + // initiated some scrolling operation. We're responding to that event here. + if (_lastScrollOffset.has_value()) + { + // If this event's offset is the same as the last offset message + // we've sent, then clear out the expected offset. We do this + // because in that case, the message we're replying to was the + // last scroll event we raised. + // Regardless, we're going to ignore this message, because the + // terminal is already in the scroll position it wants. + const auto ourLastOffset = _lastScrollOffset.value(); + if (newValue == ourLastOffset) + { + _lastScrollOffset = std::nullopt; + } + } + else + { + // This is a scroll event that wasn't initiated by the termnial + // itself - it was initiated by the mouse wheel, or the scrollbar. + this->ScrollViewport(static_cast(newValue)); + } + } + + // Method Description: + // - captures the pointer so that none of the other XAML elements respond to pointer events + // Arguments: + // - sender: XAML element that is interacting with pointer + // - args: pointer data (i.e.: mouse, touch) + // Return Value: + // - true if we successfully capture the pointer, false otherwise. + bool TermControl::_CapturePointer(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& args) + { + IUIElement uielem; + if (sender.try_as(uielem)) + { + uielem.CapturePointer(args.Pointer()); + return true; + } + return false; + } + + // Method Description: + // - releases the captured pointer because we're done responding to XAML pointer events + // Arguments: + // - sender: XAML element that is interacting with pointer + // - args: pointer data (i.e.: mouse, touch) + // Return Value: + // - true if we release capture of the pointer, false otherwise. + bool TermControl::_ReleasePointerCapture(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& args) + { + IUIElement uielem; + if (sender.try_as(uielem)) + { + uielem.ReleasePointerCapture(args.Pointer()); + return true; + } + return false; + } + + // Method Description: + // - Starts new pointer related auto scroll behavior, or continues existing one. + // Does nothing when there is already auto scroll associated with another pointer. + // Arguments: + // - pointerPoint: info about pointer that causes auto scroll. Pointer's position + // is later used to update selection. + // - scrollVelocity: target velocity of scrolling in characters / sec + void TermControl::_TryStartAutoScroll(Windows::UI::Input::PointerPoint const& pointerPoint, const double scrollVelocity) + { + // Allow only one pointer at the time + if (!_autoScrollingPointerPoint.has_value() || _autoScrollingPointerPoint.value().PointerId() == pointerPoint.PointerId()) + { + _autoScrollingPointerPoint = pointerPoint; + _autoScrollVelocity = scrollVelocity; + + // If this is first time the auto scroll update is about to be called, + // kick-start it by initializing its time delta as if it started now + if (!_lastAutoScrollUpdateTime.has_value()) + { + _lastAutoScrollUpdateTime = std::chrono::high_resolution_clock::now(); + } + + // Apparently this check is not necessary but greatly improves performance + if (!_autoScrollTimer.IsEnabled()) + { + _autoScrollTimer.Start(); + } + } + } + + // Method Description: + // - Stops auto scroll if it's active and is associated with supplied pointer id. + // Arguments: + // - pointerId: id of pointer for which to stop auto scroll + void TermControl::_TryStopAutoScroll(const uint32_t pointerId) + { + if (_autoScrollingPointerPoint.has_value() && pointerId == _autoScrollingPointerPoint.value().PointerId()) + { + _autoScrollingPointerPoint = std::nullopt; + _autoScrollVelocity = 0; + _lastAutoScrollUpdateTime = std::nullopt; + + // Apparently this check is not necessary but greatly improves performance + if (_autoScrollTimer.IsEnabled()) + { + _autoScrollTimer.Stop(); + } + } + } + + // Method Description: + // - Called continuously to gradually scroll viewport when user is + // mouse selecting outside it (to 'follow' the cursor). + // Arguments: + // - none + void TermControl::_UpdateAutoScroll(Windows::Foundation::IInspectable const& /* sender */, + Windows::Foundation::IInspectable const& /* e */) + { + if (_autoScrollVelocity != 0) + { + const auto timeNow = std::chrono::high_resolution_clock::now(); + + if (_lastAutoScrollUpdateTime.has_value()) + { + static constexpr double microSecPerSec = 1000000.0; + const double deltaTime = std::chrono::duration_cast(timeNow - _lastAutoScrollUpdateTime.value()).count() / microSecPerSec; + _scrollBar.Value(_scrollBar.Value() + _autoScrollVelocity * deltaTime); + + if (_autoScrollingPointerPoint.has_value()) + { + _SetEndSelectionPointAtCursor(_autoScrollingPointerPoint.value().Position()); + } + } + + _lastAutoScrollUpdateTime = timeNow; + } + } + + // Method Description: + // - Event handler for the GotFocus event. This is used to start + // blinking the cursor when the window is focused. + void TermControl::_GotFocusHandler(Windows::Foundation::IInspectable const& /* sender */, + RoutedEventArgs const& /* args */) + { + if (_closing) + { + return; + } + _focused = true; + + if (_tsfInputControl != nullptr) + { + _tsfInputControl.NotifyFocusEnter(); + } + + if (_cursorTimer.has_value()) + { + _cursorTimer.value().Start(); + } + } + + // Method Description: + // - Event handler for the LostFocus event. This is used to hide + // and stop blinking the cursor when the window loses focus. + void TermControl::_LostFocusHandler(Windows::Foundation::IInspectable const& /* sender */, + RoutedEventArgs const& /* args */) + { + if (_closing) + { + return; + } + _focused = false; + + if (_tsfInputControl != nullptr) + { + _tsfInputControl.NotifyFocusLeave(); + } + + if (_cursorTimer.has_value()) + { + _cursorTimer.value().Stop(); + _terminal->SetCursorVisible(false); + } + } + + void TermControl::_SendInputToConnection(const std::wstring& wstr) + { + _connection.WriteInput(wstr); + } + + // Method Description: + // - Pre-process text pasted (presumably from the clipboard) + // before sending it over the terminal's connection, converting + // Windows-space \r\n line-endings to \r line-endings + void TermControl::_SendPastedTextToConnection(const std::wstring& wstr) + { + // Some notes on this implementation: + // + // - std::regex can do this in a single line, but is somewhat + // overkill for a simple search/replace operation (and its + // performance guarantees aren't exactly stellar) + // - The STL doesn't have a simple string search/replace method. + // This fact is lamentable. + // - This line-ending converstion is intentionally fairly + // conservative, to avoid stripping out lone \n characters + // where they could conceivably be intentional. + + std::wstring stripped{ wstr }; + + std::wstring::size_type pos = 0; + + while ((pos = stripped.find(L"\r\n", pos)) != std::wstring::npos) + { + stripped.replace(pos, 2, L"\r"); + } + + _connection.WriteInput(stripped); + _terminal->TrySnapOnInput(); + } + + // Method Description: + // - Update the font with the renderer. This will be called either when the + // font changes or the DPI changes, as DPI changes will necessitate a + // font change. This method will *not* change the buffer/viewport size + // to account for the new glyph dimensions. Callers should make sure to + // appropriately call _DoResize after this method is called. + void TermControl::_UpdateFont() + { + auto lock = _terminal->LockForWriting(); + + const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * _swapChainPanel.CompositionScaleX()); + + // 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); + } + + // Method Description: + // - Set the font size of the terminal control. + // Arguments: + // - fontSize: The size of the font. + void TermControl::_SetFontSize(int fontSize) + { + try + { + // Make sure we have a non-zero font size + const auto newSize = std::max(gsl::narrow(fontSize), static_cast(1)); + const auto* fontFace = _settings.FontFace().c_str(); + _actualFont = { fontFace, 0, 10, { 0, newSize }, CP_UTF8, false }; + _desiredFont = { _actualFont }; + + // Refresh our font with the renderer + _UpdateFont(); + // Resize the terminal's BUFFER to match the new font size. This does + // NOT change the size of the window, because that can lead to more + // problems (like what happens when you change the font size while the + // window is maximized?) + auto lock = _terminal->LockForWriting(); + _DoResize(_swapChainPanel.ActualWidth(), _swapChainPanel.ActualHeight()); + } + CATCH_LOG(); + } + + // Method Description: + // - Triggered when the swapchain changes size. We use this to resize the + // terminal buffers to match the new visible size. + // Arguments: + // - e: a SizeChangedEventArgs with the new dimensions of the SwapChainPanel + void TermControl::_SwapChainSizeChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, + SizeChangedEventArgs const& e) + { + if (!_initializedTerminal) + { + return; + } + + auto lock = _terminal->LockForWriting(); + + const auto foundationSize = e.NewSize(); + + _DoResize(foundationSize.Width, foundationSize.Height); + } + + void TermControl::_SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, + Windows::Foundation::IInspectable const& /*args*/) + { + const auto scale = sender.CompositionScaleX(); + const auto dpi = (int)(scale * USER_DEFAULT_SCREEN_DPI); + + // TODO: MSFT: 21169071 - Shouldn't this all happen through _renderer and trigger the invalidate automatically on DPI change? + THROW_IF_FAILED(_renderEngine->UpdateDpi(dpi)); + _renderer->TriggerRedrawAll(); + } + + // Method Description: + // - Toggle the cursor on and off when called by the cursor blink timer. + // Arguments: + // - sender: not used + // - e: not used + void TermControl::_BlinkCursor(Windows::Foundation::IInspectable const& /* sender */, + Windows::Foundation::IInspectable const& /* e */) + { + if ((_closing) || (!_terminal->IsCursorBlinkingAllowed() && _terminal->IsCursorVisible())) + { + return; + } + _terminal->SetCursorVisible(!_terminal->IsCursorVisible()); + } + + // Method Description: + // - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging. + // Arguments: + // - cursorPosition: in pixels, relative to the origin of the control + void TermControl::_SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition) + { + auto terminalPosition = _GetTerminalPosition(cursorPosition); + + const short lastVisibleRow = std::max(_terminal->GetViewport().Height() - 1, 0); + const short lastVisibleCol = std::max(_terminal->GetViewport().Width() - 1, 0); + + terminalPosition.Y = std::clamp(terminalPosition.Y, 0, lastVisibleRow); + terminalPosition.X = std::clamp(terminalPosition.X, 0, lastVisibleCol); + + // save location (for rendering) + render + _terminal->SetEndSelectionPosition(terminalPosition); + _renderer->TriggerSelection(); + } + + // Method Description: + // - Process a resize event that was initiated by the user. This can either + // be due to the user resizing the window (causing the swapchain to + // resize) or due to the DPI changing (causing us to need to resize the + // buffer to match) + // Arguments: + // - newWidth: the new width of the swapchain, in pixels. + // - newHeight: the new height of the swapchain, in pixels. + void TermControl::_DoResize(const double newWidth, const double newHeight) + { + SIZE size; + size.cx = static_cast(newWidth); + size.cy = static_cast(newHeight); + + // Don't actually resize so small that a single character wouldn't fit + // in either dimension. The buffer really doesn't like being size 0. + if (size.cx < _actualFont.GetSize().X || size.cy < _actualFont.GetSize().Y) + { + return; + } + + // Tell the dx engine that our window is now the new size. + THROW_IF_FAILED(_renderEngine->SetWindowSize(size)); + + // Invalidate everything + _renderer->TriggerRedrawAll(); + + // Convert our new dimensions to characters + const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, + { static_cast(size.cx), static_cast(size.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 + const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() }); + if (SUCCEEDED(hr) && hr != S_FALSE) + { + _connection.Resize(vp.Height(), vp.Width()); + } + } + + void TermControl::_TerminalTitleChanged(const std::wstring_view& wstr) + { + _titleChangedHandlers(winrt::hstring{ wstr }); + } + + // Method Description: + // - Update the postion and size of the scrollbar to match the given + // viewport top, viewport height, and buffer size. + // The change will be actually handled in _ScrollbarChangeHandler. + // This should be done on the UI thread. Make sure the caller is calling + // us in a RunAsync block. + // Arguments: + // - viewTop: the top of the visible viewport, in rows. 0 indicates the top + // of the buffer. + // - viewHeight: the height of the viewport in rows. + // - bufferSize: the length of the buffer, in rows + void TermControl::_ScrollbarUpdater(Controls::Primitives::ScrollBar scrollBar, + const int viewTop, + const int viewHeight, + const int bufferSize) + { + const auto hiddenContent = bufferSize - viewHeight; + scrollBar.Maximum(hiddenContent); + scrollBar.Minimum(0); + scrollBar.ViewportSize(viewHeight); + + scrollBar.Value(viewTop); + } + + // Method Description: + // - Update the postion and size of the scrollbar to match the given + // viewport top, viewport height, and buffer size. + // Additionally fires a ScrollPositionChanged event for anyone who's + // registered an event handler for us. + // Arguments: + // - viewTop: the top of the visible viewport, in rows. 0 indicates the top + // of the buffer. + // - viewHeight: the height of the viewport in rows. + // - bufferSize: the length of the buffer, in rows + void TermControl::_TerminalScrollPositionChanged(const int viewTop, + const int viewHeight, + const int bufferSize) + { + // Since this callback fires from non-UI thread, we might be already + // closed/closing. + if (_closing.load()) + { + return; + } + + // Update our scrollbar + _scrollBar.Dispatcher().RunAsync(CoreDispatcherPriority::Low, [=]() { + // Even if we weren't closed/closing few lines above, we might be + // while waiting for this block of code to be dispatched. + if (_closing.load()) + { + return; + } + + _ScrollbarUpdater(_scrollBar, viewTop, viewHeight, bufferSize); + }); + + // Set this value as our next expected scroll position. + _lastScrollOffset = { viewTop }; + _scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize); + } + + hstring TermControl::Title() + { + if (!_initializedTerminal) + return L""; + + hstring hstr(_terminal->GetConsoleTitle()); + return hstr; + } + + // Method Description: + // - Given a copy-able selection, get the selected text from the buffer and send it to the + // Windows Clipboard (CascadiaWin32:main.cpp). + // - CopyOnSelect does NOT clear the selection + // Arguments: + // - trimTrailingWhitespace: enable removing any whitespace from copied selection + // and get text to appear on separate lines. + bool TermControl::CopySelectionToClipboard(bool trimTrailingWhitespace) + { + // no selection --> nothing to copy + if (_terminal == nullptr || !_terminal->IsSelectionActive()) + { + return false; + } + // extract text from buffer + const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace); + + // convert text: vector --> string + std::wstring textData; + for (const auto& text : bufferData.text) + { + textData += text; + } + + // convert text to HTML format + const auto htmlData = TextBuffer::GenHTML(bufferData, + _actualFont.GetUnscaledSize().Y, + _actualFont.GetFaceName(), + _settings.DefaultBackground(), + "Windows Terminal"); + + // convert to RTF format + const auto rtfData = TextBuffer::GenRTF(bufferData, + _actualFont.GetUnscaledSize().Y, + _actualFont.GetFaceName(), + _settings.DefaultBackground()); + + if (!_terminal->IsCopyOnSelectActive()) + { + _terminal->ClearSelection(); + } + + // send data up for clipboard + auto copyArgs = winrt::make_self(winrt::hstring(textData.data(), gsl::narrow(textData.size())), + winrt::to_hstring(htmlData), + winrt::to_hstring(rtfData)); + _clipboardCopyHandlers(*this, *copyArgs); + return true; + } + + // Method Description: + // - Initiate a paste operation. + void TermControl::PasteTextFromClipboard() + { + // attach TermControl::_SendInputToConnection() as the clipboardDataHandler. + // This is called when the clipboard data is loaded. + auto clipboardDataHandler = std::bind(&TermControl::_SendPastedTextToConnection, this, std::placeholders::_1); + auto pasteArgs = winrt::make_self(clipboardDataHandler); + + // send paste event up to TermApp + _clipboardPasteHandlers(*this, *pasteArgs); + } + + void TermControl::Close() + { + if (!_closing.exchange(true)) + { + // Stop accepting new output and state changes before we disconnect everything. + _connection.TerminalOutput(_connectionOutputEventToken); + _connectionStateChangedRevoker.revoke(); + + // Clear out the cursor timer, so it doesn't trigger again on us once we're destructed. + if (auto localCursorTimer{ std::exchange(_cursorTimer, std::nullopt) }) + { + localCursorTimer->Stop(); + // cursorTimer timer, now stopped, is destroyed. + } + + if (auto localAutoScrollTimer{ std::exchange(_autoScrollTimer, nullptr) }) + { + localAutoScrollTimer.Stop(); + // _autoScrollTimer timer, now stopped, is destroyed. + } + + if (auto localConnection{ std::exchange(_connection, nullptr) }) + { + localConnection.Close(); + // connection is destroyed. + } + + if (auto localRenderEngine{ std::exchange(_renderEngine, nullptr) }) + { + if (auto localRenderer{ std::exchange(_renderer, nullptr) }) + { + localRenderer->TriggerTeardown(); + // renderer is destroyed + } + // renderEngine is destroyed + } + + if (auto localTerminal{ std::exchange(_terminal, nullptr) }) + { + _initializedTerminal = false; + // terminal is destroyed. + } + } + } + + void TermControl::ScrollViewport(int viewTop) + { + _terminal->UserScrollViewport(viewTop); + } + + // Method Description: + // - Scrolls the viewport of the terminal and updates the scroll bar accordingly + // Arguments: + // - viewTop: the viewTop to scroll to + // The difference between this function and ScrollViewport is that this one also + // updates the _scrollBar after the viewport scroll. The reason _scrollBar is not updated in + // ScrollViewport is because ScrollViewport is being called by _ScrollbarChangeHandler + void TermControl::KeyboardScrollViewport(int viewTop) + { + _terminal->UserScrollViewport(viewTop); + _lastScrollOffset = std::nullopt; + _scrollBar.Value(static_cast(viewTop)); + } + + int TermControl::GetScrollOffset() + { + return _terminal->GetScrollOffset(); + } + + // Function Description: + // - Gets the height of the terminal in lines of text + // Return Value: + // - The height of the terminal in lines of text + int TermControl::GetViewHeight() const + { + const auto viewPort = _terminal->GetViewport(); + return viewPort.Height(); + } + + // Function Description: + // - Determines how much space (in pixels) an app would need to reserve to + // create a control with the settings stored in the settings param. This + // accounts for things like the font size and face, the initialRows and + // initialCols, and scrollbar visibility. The returned sized is based upon + // the provided DPI value + // Arguments: + // - settings: A IControlSettings with the settings to get the pixel size of. + // - dpi: The DPI we should create the terminal at. This affects things such + // as font size, scrollbar and other control scaling, etc. Make sure the + // caller knows what monitor the control is about to appear on. + // Return Value: + // - a point containing the requested dimensions in pixels. + winrt::Windows::Foundation::Point TermControl::GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi) + { + // Initialize our font information. + const auto* fontFace = settings.FontFace().c_str(); + const short fontHeight = gsl::narrow(settings.FontSize()); + // The font width doesn't terribly matter, we'll only be using the + // height to look it up + // The other params here also largely don't matter. + // The family is only used to determine if the font is truetype or + // not, but DX doesn't use that info at all. + // The Codepage is additionally not actually used by the DX engine at all. + FontInfo actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false }; + FontInfoDesired desiredFont = { actualFont }; + + // If the settings have negative or zero row or column counts, ignore those counts. + // (The lower TerminalCore layer also has upper bounds as well, but at this layer + // we may eventually impose different ones depending on how many pixels we can address.) + const auto cols = std::max(settings.InitialCols(), 1); + const auto rows = std::max(settings.InitialRows(), 1); + + // Create a DX engine and initialize it with our font and DPI. We'll + // then use it to measure how much space the requested rows and columns + // will take up. + // TODO: MSFT:21254947 - use a static function to do this instead of + // instantiating a DxEngine + auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); + THROW_IF_FAILED(dxEngine->UpdateDpi(dpi)); + THROW_IF_FAILED(dxEngine->UpdateFont(desiredFont, actualFont)); + + const float scale = dxEngine->GetScaling(); + const auto fontSize = actualFont.GetSize(); + + // Manually multiply by the scaling factor. The DX engine doesn't + // actually store the scaled font size in the fontInfo.GetSize() + // property when the DX engine is in Composition mode (which it is for + // the Terminal). At runtime, this is fine, as we'll transform + // everything by our scaling, so it'll work out. However, right now we + // need to get the exact pixel count. + const float fFontWidth = gsl::narrow(fontSize.X * scale); + const float fFontHeight = gsl::narrow(fontSize.Y * scale); + + // UWP XAML scrollbars aren't guaranteed to be the same size as the + // ComCtl scrollbars, but it's certainly close enough. + const auto scrollbarSize = GetSystemMetricsForDpi(SM_CXVSCROLL, dpi); + + double width = cols * fFontWidth; + + // Reserve additional space if scrollbar is intended to be visible + if (settings.ScrollState() == ScrollbarState::Visible) + { + width += scrollbarSize; + } + + double height = rows * fFontHeight; + auto thickness = _ParseThicknessFromPadding(settings.Padding()); + width += thickness.Left + thickness.Right; + height += thickness.Top + thickness.Bottom; + + return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; + } + + // Method Description: + // - Get the size of a single character of this control. The size is in + // DIPs. If you need it in _pixels_, you'll need to multiply by the + // current display scaling. + // Arguments: + // - + // Return Value: + // - The dimensions of a single character of this control, in DIPs + winrt::Windows::Foundation::Size TermControl::CharacterDimensions() const + { + const auto fontSize = _actualFont.GetSize(); + return { gsl::narrow_cast(fontSize.X), gsl::narrow_cast(fontSize.Y) }; + } + + // Method Description: + // - Get the absolute minimum size that this control can be resized to and + // still have 1x1 character visible. This includes the space needed for + // the scrollbar and the padding. + // Arguments: + // - + // Return Value: + // - The minimum size that this terminal control can be resized to and still + // have a visible character. + winrt::Windows::Foundation::Size TermControl::MinimumSize() const + { + const auto fontSize = _actualFont.GetSize(); + double width = fontSize.X; + double height = fontSize.Y; + // Reserve additional space if scrollbar is intended to be visible + if (_settings.ScrollState() == ScrollbarState::Visible) + { + width += _scrollBar.ActualWidth(); + } + + // Account for the size of any padding + auto thickness = _ParseThicknessFromPadding(_settings.Padding()); + width += thickness.Left + thickness.Right; + height += thickness.Top + thickness.Bottom; + + return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; + } + + // Method Description: + // - Create XAML Thickness object based on padding props provided. + // Used for controlling the TermControl XAML Grid container's Padding prop. + // Arguments: + // - padding: 2D padding values + // Single Double value provides uniform padding + // Two Double values provide isometric horizontal & vertical padding + // Four Double values provide independent padding for 4 sides of the bounding rectangle + // Return Value: + // - Windows::UI::Xaml::Thickness object + Windows::UI::Xaml::Thickness TermControl::_ParseThicknessFromPadding(const hstring padding) + { + const wchar_t singleCharDelim = L','; + std::wstringstream tokenStream(padding.c_str()); + std::wstring token; + uint8_t paddingPropIndex = 0; + std::array thicknessArr = {}; + size_t* idx = nullptr; + + // Get padding values till we run out of delimiter separated values in the stream + // or we hit max number of allowable values (= 4) for the bounding rectangle + // Non-numeral values detected will default to 0 + // std::getline will not throw exception unless flags are set on the wstringstream + // std::stod will throw invalid_argument expection if the input is an invalid double value + // std::stod will throw out_of_range expection if the input value is more than DBL_MAX + try + { + for (; std::getline(tokenStream, token, singleCharDelim) && (paddingPropIndex < thicknessArr.size()); paddingPropIndex++) + { + // std::stod internall calls wcstod which handles whitespace prefix (which is ignored) + // & stops the scan when first char outside the range of radix is encountered + // We'll be permissive till the extent that stod function allows us to be by default + // Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail + thicknessArr[paddingPropIndex] = std::stod(token, idx); + } + } + catch (...) + { + // If something goes wrong, even if due to a single bad padding value, we'll reset the index & return default 0 padding + paddingPropIndex = 0; + LOG_CAUGHT_EXCEPTION(); + } + + switch (paddingPropIndex) + { + case 1: + return ThicknessHelper::FromUniformLength(thicknessArr[0]); + case 2: + return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[0], thicknessArr[1]); + // No case for paddingPropIndex = 3, since it's not a norm to provide just Left, Top & Right padding values leaving out Bottom + case 4: + return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[2], thicknessArr[3]); + default: + return Thickness(); + } + } + + // Method Description: + // - Get the modifier keys that are currently pressed. This can be used to + // find out which modifiers (ctrl, alt, shift) are pressed in events that + // don't necessarily include that state. + // Return Value: + // - The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. + ControlKeyStates TermControl::_GetPressedModifierKeys() const + { + CoreWindow window = CoreWindow::GetForCurrentThread(); + // DONT USE + // != CoreVirtualKeyStates::None + // OR + // == CoreVirtualKeyStates::Down + // Sometimes with the key down, the state is Down | Locked. + // Sometimes with the key up, the state is Locked. + // IsFlagSet(Down) is the only correct solution. + + struct KeyModifier + { + VirtualKey vkey; + ControlKeyStates flags; + }; + + constexpr std::array modifiers{ { + { VirtualKey::RightMenu, ControlKeyStates::RightAltPressed }, + { VirtualKey::LeftMenu, ControlKeyStates::LeftAltPressed }, + { VirtualKey::RightControl, ControlKeyStates::RightCtrlPressed }, + { VirtualKey::LeftControl, ControlKeyStates::LeftCtrlPressed }, + { VirtualKey::Shift, ControlKeyStates::ShiftPressed }, + } }; + + ControlKeyStates flags; + + for (const auto& mod : modifiers) + { + const auto state = window.GetKeyState(mod.vkey); + const auto isDown = WI_IsFlagSet(state, CoreVirtualKeyStates::Down); + + if (isDown) + { + flags |= mod.flags; + } + } + + return flags; + } + + // Method Description: + // - Gets the corresponding viewport terminal position for the cursor + // by excluding the padding and normalizing with the font size. + // This is used for selection. + // Arguments: + // - cursorPosition: the (x,y) position of a given cursor (i.e.: mouse cursor). + // NOTE: origin (0,0) is top-left. + // Return Value: + // - the corresponding viewport terminal position for the given Point parameter + const COORD TermControl::_GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition) + { + // Exclude padding from cursor position calculation + COORD terminalPosition = { + static_cast(cursorPosition.X - _swapChainPanel.Margin().Left), + static_cast(cursorPosition.Y - _swapChainPanel.Margin().Top) + }; + + const auto fontSize = _actualFont.GetSize(); + FAIL_FAST_IF(fontSize.X == 0); + FAIL_FAST_IF(fontSize.Y == 0); + + // Normalize to terminal coordinates by using font size + terminalPosition.X /= fontSize.X; + terminalPosition.Y /= fontSize.Y; + + return terminalPosition; + } + + // Method Description: + // - Composition Completion handler for the TSFInputControl that + // handles writing text out to TerminalConnection + // Arguments: + // - text: the text to write to TerminalConnection + // Return Value: + // - + void TermControl::_CompositionCompleted(winrt::hstring text) + { + _connection.WriteInput(text); + } + + // Method Description: + // - CurrentCursorPosition handler for the TSFInputControl that + // handles returning current cursor position. + // Arguments: + // - eventArgs: event for storing the current cursor position + // Return Value: + // - + void TermControl::_CurrentCursorPositionHandler(const IInspectable& /*sender*/, const CursorPositionEventArgs& eventArgs) + { + const COORD cursorPos = _terminal->GetCursorPosition(); + Windows::Foundation::Point p = { gsl::narrow(cursorPos.X), gsl::narrow(cursorPos.Y) }; + eventArgs.CurrentPosition(p); + } + + // Method Description: + // - FontInfo handler for the TSFInputControl that + // handles returning current font information + // Arguments: + // - eventArgs: event for storing the current font information + // Return Value: + // - + void TermControl::_FontInfoHandler(const IInspectable& /*sender*/, const FontInfoEventArgs& eventArgs) + { + eventArgs.FontSize(CharacterDimensions()); + eventArgs.FontFace(_actualFont.GetFaceName()); + } + + // Method Description: + // - Returns the number of clicks that occurred (double and triple click support) + // Arguments: + // - clickPos: the (x,y) position of a given cursor (i.e.: mouse cursor). + // NOTE: origin (0,0) is top-left. + // - clickTime: the timestamp that the click occurred + // Return Value: + // - if the click is in the same position as the last click and within the timeout, the number of clicks within that time window + // - otherwise, 1 + const unsigned int TermControl::_NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime) + { + // if click occurred at a different location or past the multiClickTimer... + Timestamp delta; + THROW_IF_FAILED(UInt64Sub(clickTime, _lastMouseClick, &delta)); + if (clickPos != _lastMouseClickPos || delta > _multiClickTimer) + { + // exit early. This is a single click. + _multiClickCounter = 1; + } + else + { + _multiClickCounter++; + } + return _multiClickCounter; + } + + // Method Description: + // - Calculates speed of single axis of auto scrolling. It has to allow for both + // fast and precise selection. + // Arguments: + // - cursorDistanceFromBorder: distance from viewport border to cursor, in pixels. Must be non-negative. + // Return Value: + // - positive speed in characters / sec + double TermControl::_GetAutoScrollSpeed(double cursorDistanceFromBorder) const + { + // The numbers below just feel well, feel free to change. + // TODO: Maybe account for space beyond border that user has available + return std::pow(cursorDistanceFromBorder, 2.0) / 25.0 + 2.0; + } + + // -------------------------------- WinRT Events --------------------------------- + // Winrt events need a method for adding a callback to the event and removing the callback. + // These macros will define them both for you. + DEFINE_EVENT(TermControl, TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs); + DEFINE_EVENT(TermControl, ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs); + + DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, PasteFromClipboard, _clipboardPasteHandlers, TerminalControl::TermControl, TerminalControl::PasteFromClipboardEventArgs); + DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, CopyToClipboard, _clipboardCopyHandlers, TerminalControl::TermControl, TerminalControl::CopyToClipboardEventArgs); + // clang-format on +} From 37b408d3ca57bc0b9236a3c03b55d18dd985d4cf Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Mon, 2 Dec 2019 17:12:35 +1300 Subject: [PATCH 05/13] Fixed whitespace in file for better diff in PR. --- src/cascadia/TerminalControl/TermControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 4cf2d7afd27..d1f6efd7343 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -264,7 +264,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _tsfInputControl.Margin(newMargin); // set number of rows to scroll at a time - _rowsToScroll = _settings.RowsToScroll(); + _rowsToScroll = _settings.RowsToScroll(); } // Method Description: From 5a3cd445ba6b6f6037a6a4e4b42eec64b4a12bfb Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Wed, 4 Dec 2019 18:45:48 +1300 Subject: [PATCH 06/13] Terminal now uses system settings for number of lines to scroll if it's not overridden in settings. --- src/cascadia/TerminalApp/defaults.json | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 17 +++++++++++++++-- src/cascadia/TerminalControl/TermControl.h | 2 ++ .../TerminalSettings/TerminalSettings.cpp | 2 +- src/inc/DefaultSettings.h | 2 +- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/cascadia/TerminalApp/defaults.json b/src/cascadia/TerminalApp/defaults.json index ad814ed388f..eb4e7f1bbc2 100644 --- a/src/cascadia/TerminalApp/defaults.json +++ b/src/cascadia/TerminalApp/defaults.json @@ -4,7 +4,7 @@ "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", "initialCols": 120, "initialRows": 30, - "rowsToScroll" : 4, + "rowsToScroll" : null, "requestedTheme": "system", "showTabsInTitlebar": true, "showTerminalTitleInTitlebar": true, diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index d1f6efd7343..53ad228a2f1 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1034,8 +1034,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // With one of the precision mouses, one click is always a multiple of 120, // but the "smooth scrolling" mode results in non-int values - // Number of rows to scroll is now populated from settings. - double newValue = (_rowsToScroll * rowDelta) + (currentOffset); + // Number of rows to scroll is now populated from system default, but can be overridden in the settings file. + double newValue = (DetermineRowsToScroll() * rowDelta) + (currentOffset); // Clear our expected scroll offset. The viewport will now move in // response to our user input. @@ -1052,6 +1052,19 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation } } + int32_t TermControl::DetermineRowsToScroll() + { + //if the user hasn't overriden rowsToScroll in the settings, or specified 0 (NULL), we'll grab the value from the system setting. + if (_rowsToScroll != NULL) + return _rowsToScroll; + + uint32_t scrollLines; + if (SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &scrollLines, 0)) + return scrollLines; + + return 4; + } + void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/, Controls::Primitives::RangeBaseValueChangedEventArgs const& args) { diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index a05522ec636..60813066bd3 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -209,6 +209,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void _CompositionCompleted(winrt::hstring text); void _CurrentCursorPositionHandler(const IInspectable& /*sender*/, const CursorPositionEventArgs& eventArgs); void _FontInfoHandler(const IInspectable& /*sender*/, const FontInfoEventArgs& eventArgs); + + int32_t DetermineRowsToScroll(); }; } diff --git a/src/cascadia/TerminalSettings/TerminalSettings.cpp b/src/cascadia/TerminalSettings/TerminalSettings.cpp index 514e08dbfc0..9bd723909cc 100644 --- a/src/cascadia/TerminalSettings/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettings/TerminalSettings.cpp @@ -17,7 +17,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation _historySize{ DEFAULT_HISTORY_SIZE }, _initialRows{ 30 }, _initialCols{ 80 }, - _rowsToScroll{ 4 }, + _rowsToScroll{ NULL }, _snapOnInput{ true }, _cursorColor{ DEFAULT_CURSOR_COLOR }, _cursorShape{ CursorStyle::Vintage }, diff --git a/src/inc/DefaultSettings.h b/src/inc/DefaultSettings.h index 0e731f044a1..cb30265de0f 100644 --- a/src/inc/DefaultSettings.h +++ b/src/inc/DefaultSettings.h @@ -35,7 +35,7 @@ constexpr int DEFAULT_FONT_SIZE = 12; constexpr int DEFAULT_ROWS = 30; constexpr int DEFAULT_COLS = 120; -constexpr int DEFAULT_ROWSTOSCROLL = 4; +constexpr int DEFAULT_ROWSTOSCROLL = NULL; const std::wstring DEFAULT_PADDING{ L"8, 8, 8, 8" }; const std::wstring DEFAULT_STARTING_DIRECTORY{ L"%USERPROFILE%" }; From d9bbe0da906bb0b0e5e5d9a371976ba0db1a9621 Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Thu, 5 Dec 2019 09:54:32 +1300 Subject: [PATCH 07/13] Terminal control will only grab system setting for lines to scroll on focus, not on every scroll event. More efficient. --- src/cascadia/TerminalControl/TermControl.cpp | 16 ++++++---------- src/cascadia/TerminalControl/TermControl.h | 3 ++- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 53ad228a2f1..086c13316af 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -264,7 +264,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _tsfInputControl.Margin(newMargin); // set number of rows to scroll at a time - _rowsToScroll = _settings.RowsToScroll(); + _settingsRowsToScroll = _settings.RowsToScroll(); } // Method Description: @@ -1055,14 +1055,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation int32_t TermControl::DetermineRowsToScroll() { //if the user hasn't overriden rowsToScroll in the settings, or specified 0 (NULL), we'll grab the value from the system setting. - if (_rowsToScroll != NULL) - return _rowsToScroll; - - uint32_t scrollLines; - if (SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &scrollLines, 0)) - return scrollLines; - - return 4; + return (_settingsRowsToScroll != NULL) ? _settingsRowsToScroll : _systemRowsToScroll; } void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/, @@ -1210,7 +1203,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // Method Description: // - Event handler for the GotFocus event. This is used to start - // blinking the cursor when the window is focused. + // blinking the cursor when the window is focused, and update the number of lines to scroll set in the system void TermControl::_GotFocusHandler(Windows::Foundation::IInspectable const& /* sender */, RoutedEventArgs const& /* args */) { @@ -1229,6 +1222,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation { _cursorTimer.value().Start(); } + + if (!SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &_systemRowsToScroll, 0)) + _systemRowsToScroll = 4; } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 60813066bd3..aaff5cfc68d 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -121,7 +121,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation FontInfoDesired _desiredFont; FontInfo _actualFont; - int _rowsToScroll; + int _settingsRowsToScroll; + uint32_t _systemRowsToScroll; std::optional _lastScrollOffset; From 7a4ff11f7286e0210fc6bf5f6b1688ef457d5964 Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Thu, 12 Dec 2019 12:27:55 +1300 Subject: [PATCH 08/13] Updated settings schema and addressed feedback. --- doc/cascadia/SettingsSchema.md | 1 + doc/cascadia/profiles.schema.json | 7 +++++++ src/cascadia/TerminalApp/defaults.json | 3 +-- src/cascadia/TerminalControl/TermControl.cpp | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/doc/cascadia/SettingsSchema.md b/doc/cascadia/SettingsSchema.md index 64b6ad59b7d..c9dbcc35ba0 100644 --- a/doc/cascadia/SettingsSchema.md +++ b/doc/cascadia/SettingsSchema.md @@ -10,6 +10,7 @@ Properties listed below affect the entire window, regardless of the profile sett | `defaultProfile` | _Required_ | String | PowerShell guid | Sets the default profile. Opens by typing Ctrl + T or by clicking the '+' icon. The guid of the desired default profile is used as the value. | | `initialCols` | _Required_ | Integer | `120` | The number of columns displayed in the window upon first load. | | `initialRows` | _Required_ | Integer | `30` | The number of rows displayed in the window upon first load. | +| `rowsToScroll` | Optional | Integer | `system` | The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is specified and can be parsed to non-zero integer | | `requestedTheme` | _Required_ | String | `system` | Sets the theme of the application. Possible values: `"light"`, `"dark"`, `"system"` | | `showTerminalTitleInTitlebar` | _Required_ | Boolean | `true` | When set to `true`, titlebar displays the title of the selected tab. When set to `false`, titlebar displays "Windows Terminal". | | `showTabsInTitlebar` | Optional | Boolean | `true` | When set to `true`, the tabs are moved into the titlebar and the titlebar disappears. When set to `false`, the titlebar sits above the tabs. | diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index aca348d7135..c5c8b8ca7d0 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -284,6 +284,13 @@ "minimum": 1, "type": "integer" }, + "rowsToScroll": { + "default": "system", + "description": "The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is specified and can be parsed to a non-zero integer.", + "maximum": 999, + "minimum": 0, + "type": "integer" + }, "keybindings": { "description": "Properties are specific to each custom key binding.", "items": { diff --git a/src/cascadia/TerminalApp/defaults.json b/src/cascadia/TerminalApp/defaults.json index c5a41057b06..e8f5130ab0d 100644 --- a/src/cascadia/TerminalApp/defaults.json +++ b/src/cascadia/TerminalApp/defaults.json @@ -3,8 +3,7 @@ "alwaysShowTabs": true, "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", "initialCols": 120, - "initialRows": 30, - "rowsToScroll" : null, + "initialRows": 30, "requestedTheme": "system", "showTabsInTitlebar": true, "showTerminalTitleInTitlebar": true, diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 86e27ccd3be..43eab05f2c7 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1241,7 +1241,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation } if (!SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &_systemRowsToScroll, 0)) + { _systemRowsToScroll = 4; + } } // Method Description: From b89d7f7d60ebb6f6dcd0440d8b4b88d553b3514a Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Thu, 12 Dec 2019 12:47:15 +1300 Subject: [PATCH 09/13] Updated settings schema docs. --- doc/cascadia/SettingsSchema.md | 2 +- doc/cascadia/profiles.schema.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/cascadia/SettingsSchema.md b/doc/cascadia/SettingsSchema.md index c9dbcc35ba0..f8cc664af38 100644 --- a/doc/cascadia/SettingsSchema.md +++ b/doc/cascadia/SettingsSchema.md @@ -10,7 +10,7 @@ Properties listed below affect the entire window, regardless of the profile sett | `defaultProfile` | _Required_ | String | PowerShell guid | Sets the default profile. Opens by typing Ctrl + T or by clicking the '+' icon. The guid of the desired default profile is used as the value. | | `initialCols` | _Required_ | Integer | `120` | The number of columns displayed in the window upon first load. | | `initialRows` | _Required_ | Integer | `30` | The number of rows displayed in the window upon first load. | -| `rowsToScroll` | Optional | Integer | `system` | The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is specified and can be parsed to non-zero integer | +| `rowsToScroll` | Optional | Integer | `null` | The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is specified and is not 0 or null. | | `requestedTheme` | _Required_ | String | `system` | Sets the theme of the application. Possible values: `"light"`, `"dark"`, `"system"` | | `showTerminalTitleInTitlebar` | _Required_ | Boolean | `true` | When set to `true`, titlebar displays the title of the selected tab. When set to `false`, titlebar displays "Windows Terminal". | | `showTabsInTitlebar` | Optional | Boolean | `true` | When set to `true`, the tabs are moved into the titlebar and the titlebar disappears. When set to `false`, the titlebar sits above the tabs. | diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index c5c8b8ca7d0..ec81fecb051 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -285,8 +285,8 @@ "type": "integer" }, "rowsToScroll": { - "default": "system", - "description": "The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is specified and can be parsed to a non-zero integer.", + "default": null, + "description": "The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is specified and is not 0 or null.", "maximum": 999, "minimum": 0, "type": "integer" From 892265d2c3d6a1cacc7ac68afdb115e9cae63c54 Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Thu, 12 Dec 2019 12:51:51 +1300 Subject: [PATCH 10/13] Performed code format --- src/cascadia/TerminalControl/TermControl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 43eab05f2c7..7c40406bf21 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1215,7 +1215,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // - Event handler for the GotFocus event. This is used to... // - enable accessibility notifications for this TermControl // - start blinking the cursor when the window is focused - // - update the number of lines to scroll to the value set in the system + // - update the number of lines to scroll to the value set in the system void TermControl::_GotFocusHandler(Windows::Foundation::IInspectable const& /* sender */, RoutedEventArgs const& /* args */) { From 0e97d7966730c126d7d86a4ceeaff057e4edfd55 Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Fri, 13 Dec 2019 14:27:08 +1300 Subject: [PATCH 11/13] Moved determination of system scrolllines to settings, and enabled use of 'system' for default in settings. --- doc/cascadia/SettingsSchema.md | 2 +- doc/cascadia/profiles.schema.json | 4 ++-- src/cascadia/TerminalApp/GlobalAppSettings.cpp | 10 +++++++++- src/cascadia/TerminalApp/defaults.json | 2 +- src/cascadia/TerminalControl/TermControl.cpp | 17 +++-------------- src/cascadia/TerminalControl/TermControl.h | 5 +---- .../TerminalSettings/TerminalSettings.cpp | 13 +++++++++++-- src/inc/DefaultSettings.h | 2 +- 8 files changed, 29 insertions(+), 26 deletions(-) diff --git a/doc/cascadia/SettingsSchema.md b/doc/cascadia/SettingsSchema.md index f8cc664af38..20780ff5bad 100644 --- a/doc/cascadia/SettingsSchema.md +++ b/doc/cascadia/SettingsSchema.md @@ -10,7 +10,7 @@ Properties listed below affect the entire window, regardless of the profile sett | `defaultProfile` | _Required_ | String | PowerShell guid | Sets the default profile. Opens by typing Ctrl + T or by clicking the '+' icon. The guid of the desired default profile is used as the value. | | `initialCols` | _Required_ | Integer | `120` | The number of columns displayed in the window upon first load. | | `initialRows` | _Required_ | Integer | `30` | The number of rows displayed in the window upon first load. | -| `rowsToScroll` | Optional | Integer | `null` | The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is specified and is not 0 or null. | +| `rowsToScroll` | Optional | Integer | `system` | The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is not zero or "system". | | `requestedTheme` | _Required_ | String | `system` | Sets the theme of the application. Possible values: `"light"`, `"dark"`, `"system"` | | `showTerminalTitleInTitlebar` | _Required_ | Boolean | `true` | When set to `true`, titlebar displays the title of the selected tab. When set to `false`, titlebar displays "Windows Terminal". | | `showTabsInTitlebar` | Optional | Boolean | `true` | When set to `true`, the tabs are moved into the titlebar and the titlebar disappears. When set to `false`, the titlebar sits above the tabs. | diff --git a/doc/cascadia/profiles.schema.json b/doc/cascadia/profiles.schema.json index ec81fecb051..c59cfd88974 100644 --- a/doc/cascadia/profiles.schema.json +++ b/doc/cascadia/profiles.schema.json @@ -285,8 +285,8 @@ "type": "integer" }, "rowsToScroll": { - "default": null, - "description": "The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is specified and is not 0 or null.", + "default": "system", + "description": "The number of rows to scroll at a time with the mouse wheel. This will override the system setting if the value is not zero or 'system'.", "maximum": 999, "minimum": 0, "type": "integer" diff --git a/src/cascadia/TerminalApp/GlobalAppSettings.cpp b/src/cascadia/TerminalApp/GlobalAppSettings.cpp index f9f86cd3360..cd7299bbf4c 100644 --- a/src/cascadia/TerminalApp/GlobalAppSettings.cpp +++ b/src/cascadia/TerminalApp/GlobalAppSettings.cpp @@ -245,7 +245,15 @@ void GlobalAppSettings::LayerJson(const Json::Value& json) } if (auto rowsToScroll{ json[JsonKey(RowsToScrollKey)] }) { - _rowsToScroll = rowsToScroll.asInt(); + //if it's not an int we fall back to setting it to 0, which implies using the system setting. This will be the case if it's set to "system" + if (rowsToScroll.isInt()) + { + _rowsToScroll = rowsToScroll.asInt(); + } + else + { + _rowsToScroll = 0; + } } if (auto initialPosition{ json[JsonKey(InitialPositionKey)] }) { diff --git a/src/cascadia/TerminalApp/defaults.json b/src/cascadia/TerminalApp/defaults.json index e8f5130ab0d..c3a311c0f72 100644 --- a/src/cascadia/TerminalApp/defaults.json +++ b/src/cascadia/TerminalApp/defaults.json @@ -3,7 +3,7 @@ "alwaysShowTabs": true, "defaultProfile": "{61c54bbd-c2c6-5271-96e7-009a87ff44bf}", "initialCols": 120, - "initialRows": 30, + "initialRows": 30, "requestedTheme": "system", "showTabsInTitlebar": true, "showTerminalTitleInTitlebar": true, diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 7c40406bf21..7a42a44119f 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -265,7 +265,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation _tsfInputControl.Margin(newMargin); // set number of rows to scroll at a time - _settingsRowsToScroll = _settings.RowsToScroll(); + _rowsToScroll = _settings.RowsToScroll(); } // Method Description: @@ -1044,8 +1044,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation // With one of the precision mouses, one click is always a multiple of 120, // but the "smooth scrolling" mode results in non-int values - // Number of rows to scroll is now populated from system default, but can be overridden in the settings file. - double newValue = (DetermineRowsToScroll() * rowDelta) + (currentOffset); + double newValue = (_rowsToScroll * rowDelta) + (currentOffset); // Clear our expected scroll offset. The viewport will now move in // response to our user input. @@ -1062,12 +1061,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation } } - int32_t TermControl::DetermineRowsToScroll() - { - //if the user hasn't overriden rowsToScroll in the settings, or specified 0 (NULL), we'll grab the value from the system setting. - return (_settingsRowsToScroll != NULL) ? _settingsRowsToScroll : _systemRowsToScroll; - } - void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/, Controls::Primitives::RangeBaseValueChangedEventArgs const& args) { @@ -1239,11 +1232,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation { _cursorTimer.value().Start(); } - - if (!SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &_systemRowsToScroll, 0)) - { - _systemRowsToScroll = 4; - } + _rowsToScroll = _settings.RowsToScroll(); } // Method Description: diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 22a98b66800..9fddb93fbef 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -123,8 +123,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation FontInfoDesired _desiredFont; FontInfo _actualFont; - int _settingsRowsToScroll; - uint32_t _systemRowsToScroll; + int _rowsToScroll; std::optional _lastScrollOffset; @@ -212,8 +211,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation void _CompositionCompleted(winrt::hstring text); void _CurrentCursorPositionHandler(const IInspectable& /*sender*/, const CursorPositionEventArgs& eventArgs); void _FontInfoHandler(const IInspectable& /*sender*/, const FontInfoEventArgs& eventArgs); - - int32_t DetermineRowsToScroll(); }; } diff --git a/src/cascadia/TerminalSettings/TerminalSettings.cpp b/src/cascadia/TerminalSettings/TerminalSettings.cpp index 9bd723909cc..79a324f2b0d 100644 --- a/src/cascadia/TerminalSettings/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettings/TerminalSettings.cpp @@ -17,7 +17,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation _historySize{ DEFAULT_HISTORY_SIZE }, _initialRows{ 30 }, _initialCols{ 80 }, - _rowsToScroll{ NULL }, + _rowsToScroll{ 0 }, _snapOnInput{ true }, _cursorColor{ DEFAULT_CURSOR_COLOR }, _cursorShape{ CursorStyle::Vintage }, @@ -113,7 +113,16 @@ namespace winrt::Microsoft::Terminal::Settings::implementation int32_t TerminalSettings::RowsToScroll() { - return _rowsToScroll; + if (_rowsToScroll != 0) + { + return _rowsToScroll; + } + int systemRowsToScroll; + if (!SystemParametersInfo(SPI_GETWHEELSCROLLLINES, 0, &systemRowsToScroll, 0)) + { + systemRowsToScroll = 4; + } + return systemRowsToScroll; } void TerminalSettings::RowsToScroll(int32_t value) diff --git a/src/inc/DefaultSettings.h b/src/inc/DefaultSettings.h index cb30265de0f..91861bf89cc 100644 --- a/src/inc/DefaultSettings.h +++ b/src/inc/DefaultSettings.h @@ -35,7 +35,7 @@ constexpr int DEFAULT_FONT_SIZE = 12; constexpr int DEFAULT_ROWS = 30; constexpr int DEFAULT_COLS = 120; -constexpr int DEFAULT_ROWSTOSCROLL = NULL; +constexpr int DEFAULT_ROWSTOSCROLL = 0; const std::wstring DEFAULT_PADDING{ L"8, 8, 8, 8" }; const std::wstring DEFAULT_STARTING_DIRECTORY{ L"%USERPROFILE%" }; From b54b8daaecfc605b4f5f6f8a61ebce8ad3db64ae Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Sun, 5 Jan 2020 09:22:09 +1300 Subject: [PATCH 12/13] Merged from master --- src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp | 4 ++-- src/cascadia/TerminalSettings/TerminalSettings.cpp | 2 +- src/cascadia/TerminalSettings/terminalsettings.h | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp index fb3dc44b499..71ed1bc1c31 100644 --- a/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp +++ b/src/cascadia/LocalTests_TerminalApp/SettingsTests.cpp @@ -442,8 +442,8 @@ namespace TerminalAppLocalTests "globals": { "showTabsInTitlebar": false, "initialCols" : 240, - "initialRows" : 60 - "rowsToScroll" : 8, + "initialRows" : 60, + "rowsToScroll" : 8 } })" }; const auto settings0Json = VerifyParseSucceeded(settings0String); diff --git a/src/cascadia/TerminalSettings/TerminalSettings.cpp b/src/cascadia/TerminalSettings/TerminalSettings.cpp index 3427386e261..969ec60e08b 100644 --- a/src/cascadia/TerminalSettings/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettings/TerminalSettings.cpp @@ -134,7 +134,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation _rowsToScroll = value; } - bool TerminalSettings::SnapOnInput() + bool TerminalSettings::SnapOnInput() noexcept { return _snapOnInput; } diff --git a/src/cascadia/TerminalSettings/terminalsettings.h b/src/cascadia/TerminalSettings/terminalsettings.h index fbaf8318547..e9cb30f150c 100644 --- a/src/cascadia/TerminalSettings/terminalsettings.h +++ b/src/cascadia/TerminalSettings/terminalsettings.h @@ -39,8 +39,8 @@ namespace winrt::Microsoft::Terminal::Settings::implementation void InitialRows(int32_t value) noexcept; int32_t InitialCols() noexcept; void InitialCols(int32_t value) noexcept; - int32_t RowsToScroll(); - void RowsToScroll(int32_t value); + int32_t RowsToScroll(); + void RowsToScroll(int32_t value); bool SnapOnInput() noexcept; void SnapOnInput(bool value) noexcept; uint32_t CursorColor() noexcept; From 069fc50f80bb01a971a5c0b48ea317c01083a2e9 Mon Sep 17 00:00:00 2001 From: Hannes Nel Date: Sun, 5 Jan 2020 10:08:02 +1300 Subject: [PATCH 13/13] Added noexcept to RowsToScroll properties. --- src/cascadia/TerminalSettings/TerminalSettings.cpp | 4 ++-- src/cascadia/TerminalSettings/terminalsettings.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cascadia/TerminalSettings/TerminalSettings.cpp b/src/cascadia/TerminalSettings/TerminalSettings.cpp index 969ec60e08b..10d7fbd7f78 100644 --- a/src/cascadia/TerminalSettings/TerminalSettings.cpp +++ b/src/cascadia/TerminalSettings/TerminalSettings.cpp @@ -115,7 +115,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation _initialCols = value; } - int32_t TerminalSettings::RowsToScroll() + int32_t TerminalSettings::RowsToScroll() noexcept { if (_rowsToScroll != 0) { @@ -129,7 +129,7 @@ namespace winrt::Microsoft::Terminal::Settings::implementation return systemRowsToScroll; } - void TerminalSettings::RowsToScroll(int32_t value) + void TerminalSettings::RowsToScroll(int32_t value) noexcept { _rowsToScroll = value; } diff --git a/src/cascadia/TerminalSettings/terminalsettings.h b/src/cascadia/TerminalSettings/terminalsettings.h index e9cb30f150c..111874131fa 100644 --- a/src/cascadia/TerminalSettings/terminalsettings.h +++ b/src/cascadia/TerminalSettings/terminalsettings.h @@ -39,8 +39,8 @@ namespace winrt::Microsoft::Terminal::Settings::implementation void InitialRows(int32_t value) noexcept; int32_t InitialCols() noexcept; void InitialCols(int32_t value) noexcept; - int32_t RowsToScroll(); - void RowsToScroll(int32_t value); + int32_t RowsToScroll() noexcept; + void RowsToScroll(int32_t value) noexcept; bool SnapOnInput() noexcept; void SnapOnInput(bool value) noexcept; uint32_t CursorColor() noexcept;