diff --git a/.github/actions/spelling/dictionary/apis.txt b/.github/actions/spelling/dictionary/apis.txt index a6087aa9271..6d75da30274 100644 --- a/.github/actions/spelling/dictionary/apis.txt +++ b/.github/actions/spelling/dictionary/apis.txt @@ -12,6 +12,7 @@ CXICON CYICON D2DERR_SHADER_COMPILE_FAILED dataobject +dcomp DERR dlldata DONTADDTORECENT @@ -127,9 +128,12 @@ wsregex wwinmain XDocument XElement +xhash xlocmes xlocmon xlocnum xloctime XParse xstring +xtree +xutility diff --git a/.vscode/settings.json b/.vscode/settings.json index 4534d560156..1b6028e5aea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -21,7 +21,20 @@ "xloctime": "cpp", "multi_span": "cpp", "pointers": "cpp", - "vector": "cpp" + "vector": "cpp", + "bitset": "cpp", + "deque": "cpp", + "initializer_list": "cpp", + "list": "cpp", + "queue": "cpp", + "random": "cpp", + "regex": "cpp", + "stack": "cpp", + "xhash": "cpp", + "xtree": "cpp", + "xutility": "cpp", + "span": "cpp", + "string_span": "cpp" }, "files.exclude": { "**/bin/**": true, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e9942e0bb6d..59291c20ce8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,7 +9,7 @@ "-Command", "Import-Module ${workspaceFolder}\\tools\\OpenConsole.psm1;", "Set-MsBuildDevEnvironment;", - "$project = switch(\"${input:buildProjectChoice}\"){OpenConsole{\"Conhost\\Host_EXE\"} Terminal{\"Terminal\\CascadiaPackage\"}};", + "$project = switch(\"${input:buildProjectChoice}\"){OpenConsole{\"Conhost\\Host_EXE\"} Terminal{\"Terminal\\CascadiaPackage\"} TermControl{\"Terminal\\TerminalControl\"}};", "$target = switch(\"${input:buildModeChoice}\"){Build{\"\"} Rebuild{\":Rebuild\"} Clean{\":Clean\"}};", "$target = $project + $target;", "msbuild", @@ -111,10 +111,11 @@ "description": "OpenConsole or Terminal?", "options":[ "OpenConsole", - "Terminal" + "Terminal", + "TermControl" ], "default": "Terminal" } ] -} \ No newline at end of file +} diff --git a/OpenConsole.sln b/OpenConsole.sln index 6c5450aacd9..2b644ae98a8 100644 --- a/OpenConsole.sln +++ b/OpenConsole.sln @@ -175,7 +175,9 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.Control. {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} = {CA5CAD1A-C46D-4588-B1C0-40F31AE9100B} {CA5CAD1A-ABCD-429C-B551-8562EC954746} = {CA5CAD1A-ABCD-429C-B551-8562EC954746} {1CF55140-EF6A-4736-A403-957E4F7430BB} = {1CF55140-EF6A-4736-A403-957E4F7430BB} + {48D21369-3D7B-4431-9967-24E81292CF62} = {48D21369-3D7B-4431-9967-24E81292CF62} {48D21369-3D7B-4431-9967-24E81292CF63} = {48D21369-3D7B-4431-9967-24E81292CF63} + {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F} = {AF0A096A-8B3A-4949-81EF-7DF8F0FEE91F} EndProjectSection EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Microsoft.Terminal.Control", "src\cascadia\TerminalControl\dll\TerminalControl.vcxproj", "{CA5CAD1A-F542-4635-A069-7CAEFB930070}" diff --git a/build/pipelines/templates/build-console-steps.yml b/build/pipelines/templates/build-console-steps.yml index 4a3adcb4054..e5455dc8c4e 100644 --- a/build/pipelines/templates/build-console-steps.yml +++ b/build/pipelines/templates/build-console-steps.yml @@ -22,7 +22,7 @@ steps: configPath: NuGet.config restoreSolution: OpenConsole.sln restoreDirectory: '$(Build.SourcesDirectory)\packages' - + - task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2 displayName: Restore NuGet packages for extraneous build actions inputs: @@ -96,7 +96,7 @@ steps: displayName: 'Upload converted test logs' inputs: testResultsFormat: 'xUnit' # Options: JUnit, NUnit, VSTest, xUnit, cTest - testResultsFiles: '**/onBuildMachineResults.xml' + testResultsFiles: '**/onBuildMachineResults.xml' #searchFolder: '$(System.DefaultWorkingDirectory)' # Optional #mergeTestResults: false # Optional #failTaskOnFailedTests: false # Optional @@ -147,4 +147,4 @@ steps: displayName: 'Publish All Build Artifacts' inputs: PathtoPublish: '$(Build.ArtifactStagingDirectory)' - ArtifactName: 'drop' \ No newline at end of file + ArtifactName: 'drop' diff --git a/src/cascadia/TerminalApp/Pane.h b/src/cascadia/TerminalApp/Pane.h index 2838bab1a86..a848ce7d905 100644 --- a/src/cascadia/TerminalApp/Pane.h +++ b/src/cascadia/TerminalApp/Pane.h @@ -19,8 +19,7 @@ // - Mike Griese (zadjii-msft) 16-May-2019 #pragma once -#include -#include + #include "../../cascadia/inc/cppwinrt_utils.h" enum class Borders : int diff --git a/src/cascadia/TerminalApp/SettingsTab.h b/src/cascadia/TerminalApp/SettingsTab.h index 1c83fc243ba..221214eff41 100644 --- a/src/cascadia/TerminalApp/SettingsTab.h +++ b/src/cascadia/TerminalApp/SettingsTab.h @@ -18,8 +18,6 @@ Author(s): #pragma once #include "TabBase.h" #include "SettingsTab.g.h" -#include -#include #include "../../cascadia/inc/cppwinrt_utils.h" namespace winrt::TerminalApp::implementation diff --git a/src/cascadia/TerminalApp/TabManagement.cpp b/src/cascadia/TerminalApp/TabManagement.cpp index f8739f1c82b..adf6d7984b6 100644 --- a/src/cascadia/TerminalApp/TabManagement.cpp +++ b/src/cascadia/TerminalApp/TabManagement.cpp @@ -153,6 +153,11 @@ namespace winrt::TerminalApp::implementation { // Possibly update the icon of the tab. page->_UpdateTabIcon(*tab); + + // Update the taskbar progress as well. We'll raise our own + // SetTaskbarProgress event here, to get tell the hosting + // application to re-query this value from us. + page->_SetTaskbarProgressHandlers(*page, nullptr); } }); diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 98818bcd61c..9094d7cc5db 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -996,9 +996,6 @@ namespace winrt::TerminalApp::implementation term.OpenHyperlink({ this, &TerminalPage::_OpenHyperlinkHandler }); - // Add an event handler for when the terminal wants to set a progress indicator on the taskbar - term.SetTaskbarProgress({ this, &TerminalPage::_SetTaskbarProgressHandler }); - term.HidePointerCursor({ get_weak(), &TerminalPage::_HidePointerCursorHandler }); term.RestorePointerCursor({ get_weak(), &TerminalPage::_RestorePointerCursorHandler }); @@ -1053,6 +1050,11 @@ namespace winrt::TerminalApp::implementation } }); + // Add an event handler for when the terminal or tab wants to set a + // progress indicator on the taskbar + hostingTab.TaskbarProgressChanged({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); + term.SetTaskbarProgress({ get_weak(), &TerminalPage::_SetTaskbarProgressHandler }); + // TODO GH#3327: Once we support colorizing the NewTab button based on // the color of the tab, we'll want to make sure to call // _ClearNewTabButtonColor here, to reset it to the default (for the @@ -1154,7 +1156,7 @@ namespace winrt::TerminalApp::implementation { // The magic value of WHEEL_PAGESCROLL indicates that we need to scroll the entire page realRowsToScroll = _systemRowsToScroll == WHEEL_PAGESCROLL ? - terminalTab->GetActiveTerminalControl().GetViewHeight() : + terminalTab->GetActiveTerminalControl().ViewHeight() : _systemRowsToScroll; } else @@ -1295,7 +1297,7 @@ namespace winrt::TerminalApp::implementation if (const auto terminalTab{ _GetFocusedTabImpl() }) { const auto control = _GetActiveControl(); - const auto termHeight = control.GetViewHeight(); + const auto termHeight = control.ViewHeight(); auto scrollDelta = _ComputeScrollDelta(scrollDirection, termHeight); terminalTab->Scroll(scrollDelta); } @@ -1647,29 +1649,37 @@ namespace winrt::TerminalApp::implementation return false; } - void TerminalPage::_ControlNoticeRaisedHandler(const IInspectable /*sender*/, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs) + // Important! Don't take this eventArgs by reference, we need to extend the + // lifetime of it to the other side of the co_await! + winrt::fire_and_forget TerminalPage::_ControlNoticeRaisedHandler(const IInspectable /*sender*/, + const Microsoft::Terminal::Control::NoticeEventArgs eventArgs) { - winrt::hstring message = eventArgs.Message(); + auto weakThis = get_weak(); + co_await winrt::resume_foreground(Dispatcher()); + if (auto page = weakThis.get()) + { + winrt::hstring message = eventArgs.Message(); - winrt::hstring title; + winrt::hstring title; - switch (eventArgs.Level()) - { - case NoticeLevel::Debug: - title = RS_(L"NoticeDebug"); //\xebe8 - break; - case NoticeLevel::Info: - title = RS_(L"NoticeInfo"); // \xe946 - break; - case NoticeLevel::Warning: - title = RS_(L"NoticeWarning"); //\xe7ba - break; - case NoticeLevel::Error: - title = RS_(L"NoticeError"); //\xe783 - break; - } + switch (eventArgs.Level()) + { + case NoticeLevel::Debug: + title = RS_(L"NoticeDebug"); //\xebe8 + break; + case NoticeLevel::Info: + title = RS_(L"NoticeInfo"); // \xe946 + break; + case NoticeLevel::Warning: + title = RS_(L"NoticeWarning"); //\xe7ba + break; + case NoticeLevel::Error: + title = RS_(L"NoticeError"); //\xe783 + break; + } - _ShowControlNoticeDialog(title, message); + page->_ShowControlNoticeDialog(title, message); + } } void TerminalPage::_ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message) @@ -1707,8 +1717,9 @@ namespace winrt::TerminalApp::implementation // Arguments: // - sender (not used) // - eventArgs: the arguments specifying how to set the progress indicator - void TerminalPage::_SetTaskbarProgressHandler(const IInspectable /*sender*/, const IInspectable /*eventArgs*/) + winrt::fire_and_forget TerminalPage::_SetTaskbarProgressHandler(const IInspectable /*sender*/, const IInspectable /*eventArgs*/) { + co_await resume_foreground(Dispatcher()); _SetTaskbarProgressHandlers(*this, nullptr); } diff --git a/src/cascadia/TerminalApp/TerminalPage.h b/src/cascadia/TerminalApp/TerminalPage.h index 060a45f99a6..8ff980e24ea 100644 --- a/src/cascadia/TerminalApp/TerminalPage.h +++ b/src/cascadia/TerminalApp/TerminalPage.h @@ -266,11 +266,11 @@ namespace winrt::TerminalApp::implementation void _ShowCouldNotOpenDialog(winrt::hstring reason, winrt::hstring uri); bool _CopyText(const bool singleLine, const Windows::Foundation::IReference& formats); - void _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs); + winrt::fire_and_forget _SetTaskbarProgressHandler(const IInspectable sender, const IInspectable eventArgs); void _PasteText(); - void _ControlNoticeRaisedHandler(const IInspectable sender, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs); + winrt::fire_and_forget _ControlNoticeRaisedHandler(const IInspectable sender, const Microsoft::Terminal::Control::NoticeEventArgs eventArgs); void _ShowControlNoticeDialog(const winrt::hstring& title, const winrt::hstring& message); fire_and_forget _LaunchSettings(const Microsoft::Terminal::Settings::Model::SettingsTarget target); diff --git a/src/cascadia/TerminalApp/TerminalTab.cpp b/src/cascadia/TerminalApp/TerminalTab.cpp index 2481fb6d320..b61a2eb6053 100644 --- a/src/cascadia/TerminalApp/TerminalTab.cpp +++ b/src/cascadia/TerminalApp/TerminalTab.cpp @@ -167,7 +167,11 @@ namespace winrt::TerminalApp::implementation if (lastFocusedControl) { lastFocusedControl.Focus(_focusState); - lastFocusedControl.TaskbarProgressChanged(); + + // Update our own progress state, and fire an event signaling + // that our taskbar progress changed. + _UpdateProgressState(); + _TaskbarProgressChangedHandlers(lastFocusedControl, nullptr); } // When we gain focus, remove the bell indicator if it is active if (_tabStatus.BellIndicator()) @@ -378,7 +382,7 @@ namespace winrt::TerminalApp::implementation co_await winrt::resume_foreground(control.Dispatcher()); - const auto currentOffset = control.GetScrollOffset(); + const auto currentOffset = control.ScrollOffset(); control.ScrollViewport(::base::ClampAdd(currentOffset, delta)); } @@ -546,6 +550,7 @@ namespace winrt::TerminalApp::implementation void TerminalTab::_AttachEventHandlersToControl(const TermControl& control) { auto weakThis{ get_weak() }; + auto dispatcher = TabViewItem().Dispatcher(); control.TitleChanged([weakThis](auto&&, auto&&) { // Check if Tab's lifetime has expired @@ -581,37 +586,12 @@ namespace winrt::TerminalApp::implementation } }); - control.SetTaskbarProgress([weakThis](auto&&, auto&&) { + control.SetTaskbarProgress([dispatcher, weakThis](auto&&, auto &&) -> winrt::fire_and_forget { + co_await winrt::resume_foreground(dispatcher); // Check if Tab's lifetime has expired if (auto tab{ weakThis.get() }) { - // The progress of the control changed, but not necessarily the progress of the tab. - // Set the tab's progress ring to the active pane's progress - if (tab->GetActiveTerminalControl().TaskbarState() > 0) - { - if (tab->GetActiveTerminalControl().TaskbarState() == 3) - { - // 3 is the indeterminate state, set the progress ring as such - tab->_tabStatus.IsProgressRingIndeterminate(true); - } - else - { - // any non-indeterminate state has a value, set the progress ring as such - tab->_tabStatus.IsProgressRingIndeterminate(false); - - const auto progressValue = gsl::narrow(tab->GetActiveTerminalControl().TaskbarProgress()); - tab->_tabStatus.ProgressValue(progressValue); - } - // Hide the tab icon (the progress ring is placed over it) - tab->HideIcon(true); - tab->_tabStatus.IsProgressRingActive(true); - } - else - { - // Show the tab icon - tab->HideIcon(false); - tab->_tabStatus.IsProgressRingActive(false); - } + tab->_UpdateProgressState(); } }); @@ -636,6 +616,54 @@ namespace winrt::TerminalApp::implementation }); } + // Method Description: + // - This should be called on the UI thread. If you don't, then it might + // silently do nothing. + // - Update our TabStatus to reflect the progress state of the currently + // active pane. + // - This is called every time _any_ control's progress state changes, + // regardless of if that control is the active one or not. This is simpler + // then re-attaching this handler to the active control each time it + // changes. + // Arguments: + // - + // Return Value: + // - + void TerminalTab::_UpdateProgressState() + { + if (const auto& activeControl{ GetActiveTerminalControl() }) + { + const auto taskbarState = activeControl.TaskbarState(); + // The progress of the control changed, but not necessarily the progress of the tab. + // Set the tab's progress ring to the active pane's progress + if (taskbarState > 0) + { + if (taskbarState == 3) + { + // 3 is the indeterminate state, set the progress ring as such + _tabStatus.IsProgressRingIndeterminate(true); + } + else + { + // any non-indeterminate state has a value, set the progress ring as such + _tabStatus.IsProgressRingIndeterminate(false); + + const auto progressValue = gsl::narrow(activeControl.TaskbarProgress()); + _tabStatus.ProgressValue(progressValue); + } + // Hide the tab icon (the progress ring is placed over it) + HideIcon(true); + _tabStatus.IsProgressRingActive(true); + } + else + { + // Show the tab icon + HideIcon(false); + _tabStatus.IsProgressRingActive(false); + } + } + } + // Method Description: // - Mark the given pane as the active pane in this tab. All other panes // will be marked as inactive. We'll also update our own UI state to @@ -653,6 +681,7 @@ namespace winrt::TerminalApp::implementation // Update our own title text to match the newly-active pane. UpdateTitle(); + _UpdateProgressState(); // We need to move the pane to the top of our mru list // If its already somewhere in the list, remove it first @@ -671,11 +700,6 @@ namespace winrt::TerminalApp::implementation _RecalculateAndApplyReadOnly(); - if (const auto control{ pane->GetTerminalControl() }) - { - control.TaskbarProgressChanged(); - } - // Raise our own ActivePaneChanged event. _ActivePaneChangedHandlers(); } diff --git a/src/cascadia/TerminalApp/TerminalTab.h b/src/cascadia/TerminalApp/TerminalTab.h index 769b9e333ed..b248dab3507 100644 --- a/src/cascadia/TerminalApp/TerminalTab.h +++ b/src/cascadia/TerminalApp/TerminalTab.h @@ -92,6 +92,7 @@ namespace winrt::TerminalApp::implementation DECLARE_EVENT(TabRaiseVisualBell, _TabRaiseVisualBellHandlers, winrt::delegate<>); DECLARE_EVENT(DuplicateRequested, _DuplicateRequestedHandlers, winrt::delegate<>); FORWARDED_TYPED_EVENT(TabRenamerDeactivated, winrt::Windows::Foundation::IInspectable, winrt::Windows::Foundation::IInspectable, (&_headerControl), RenameEnded); + TYPED_EVENT(TaskbarProgressChanged, IInspectable, IInspectable); private: std::shared_ptr _rootPane{ nullptr }; @@ -143,6 +144,8 @@ namespace winrt::TerminalApp::implementation void _RecalculateAndApplyReadOnly(); + void _UpdateProgressState(); + void _DuplicateTab(); friend class ::TerminalAppLocalTests::TabTests; diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp new file mode 100644 index 00000000000..46775a70310 --- /dev/null +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -0,0 +1,1372 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ControlCore.h" +#include +#include +#include +#include +#include +#include +#include +#include "../../types/inc/GlyphWidth.hpp" +#include "../../types/inc/Utils.hpp" +#include "../../buffer/out/search.h" + +#include "ControlCore.g.cpp" + +using namespace ::Microsoft::Console::Types; +using namespace ::Microsoft::Console::VirtualTerminal; +using namespace ::Microsoft::Terminal::Core; +using namespace winrt::Windows::Graphics::Display; +using namespace winrt::Windows::System; +using namespace winrt::Windows::ApplicationModel::DataTransfer; + +namespace winrt::Microsoft::Terminal::Control::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; + } + + ControlCore::ControlCore(IControlSettings settings, + TerminalConnection::ITerminalConnection connection) : + _connection{ connection }, + _settings{ settings }, + _desiredFont{ DEFAULT_FONT_FACE, 0, DEFAULT_FONT_WEIGHT, { 0, DEFAULT_FONT_SIZE }, CP_UTF8 }, + _actualFont{ DEFAULT_FONT_FACE, 0, DEFAULT_FONT_WEIGHT, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false } + { + _EnsureStaticInitialization(); + + _terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>(); + + // Subscribe to the connection's disconnected event and call our connection closed handlers. + _connectionStateChangedRevoker = _connection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*v*/) { + _ConnectionStateChangedHandlers(*this, nullptr); + }); + + // This event is explicitly revoked in the destructor: does not need weak_ref + _connectionOutputEventToken = _connection.TerminalOutput({ this, &ControlCore::_connectionOutputHandler }); + + _terminal->SetWriteInputCallback([this](std::wstring& wstr) { + _sendInputToConnection(wstr); + }); + + // GH#8969: pre-seed working directory to prevent potential races + _terminal->SetWorkingDirectory(_settings.StartingDirectory()); + + auto pfnCopyToClipboard = std::bind(&ControlCore::_terminalCopyToClipboard, this, std::placeholders::_1); + _terminal->SetCopyToClipboardCallback(pfnCopyToClipboard); + + auto pfnWarningBell = std::bind(&ControlCore::_terminalWarningBell, this); + _terminal->SetWarningBellCallback(pfnWarningBell); + + auto pfnTitleChanged = std::bind(&ControlCore::_terminalTitleChanged, this, std::placeholders::_1); + _terminal->SetTitleChangedCallback(pfnTitleChanged); + + auto pfnTabColorChanged = std::bind(&ControlCore::_terminalTabColorChanged, this, std::placeholders::_1); + _terminal->SetTabColorChangedCallback(pfnTabColorChanged); + + auto pfnBackgroundColorChanged = std::bind(&ControlCore::_terminalBackgroundColorChanged, this, std::placeholders::_1); + _terminal->SetBackgroundCallback(pfnBackgroundColorChanged); + + auto pfnScrollPositionChanged = std::bind(&ControlCore::_terminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); + _terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged); + + auto pfnTerminalCursorPositionChanged = std::bind(&ControlCore::_terminalCursorPositionChanged, this); + _terminal->SetCursorPositionChangedCallback(pfnTerminalCursorPositionChanged); + + auto pfnTerminalTaskbarProgressChanged = std::bind(&ControlCore::_terminalTaskbarProgressChanged, this); + _terminal->TaskbarProgressChangedCallback(pfnTerminalTaskbarProgressChanged); + + UpdateSettings(settings); + } + + ControlCore::~ControlCore() + { + Close(); + } + + bool ControlCore::Initialize(const double actualWidth, + const double actualHeight, + const double compositionScale) + { + _panelWidth = actualWidth; + _panelHeight = actualHeight; + _compositionScale = compositionScale; + + { // scope for terminalLock + auto terminalLock = _terminal->LockForWriting(); + + if (_initializedTerminal) + { + return false; + } + + const auto windowWidth = actualWidth * compositionScale; + const auto windowHeight = actualHeight * compositionScale; + + if (windowWidth == 0 || windowHeight == 0) + { + return false; + } + + // 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; + + _renderer->SetRendererEnteredErrorStateCallback([weakThis = get_weak()]() { + if (auto strongThis{ weakThis.get() }) + { + strongThis->_RendererEnteredErrorStateHandlers(*strongThis, nullptr); + } + }); + + 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(true); + + const COORD windowSize{ static_cast(windowWidth), + static_cast(windowHeight) }; + + // First 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); + LOG_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); + + // Update DxEngine's SelectionBackground + dxEngine->SetSelectionBackground(til::color{ _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); + + // IMPORTANT! Set this callback up sooner than later. If we do it + // after Enable, then it'll be possible to paint the frame once + // _before_ the warning handler is set up, and then warnings from + // the first paint will be ignored! + dxEngine->SetWarningCallback(std::bind(&ControlCore::_rendererWarning, this, std::placeholders::_1)); + + // Tell the DX Engine to notify us when the swap chain changes. + // We do this after we initially set the swapchain so as to avoid unnecessary callbacks (and locking problems) + dxEngine->SetCallback(std::bind(&ControlCore::_renderEngineSwapChainChanged, this)); + + dxEngine->SetRetroTerminalEffect(_settings.RetroTerminalEffect()); + dxEngine->SetPixelShaderPath(_settings.PixelShaderPath()); + dxEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); + dxEngine->SetSoftwareRendering(_settings.SoftwareRendering()); + + _updateAntiAliasingMode(dxEngine.get()); + + // GH#5098: Inform the engine of the opacity of the default text background. + if (_settings.UseAcrylic()) + { + dxEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); + } + + THROW_IF_FAILED(dxEngine->Enable()); + _renderEngine = std::move(dxEngine); + + _initializedTerminal = true; + } // scope for TerminalLock + + // Start the connection outside of lock, because it could + // start writing output immediately. + _connection.Start(); + + return true; + } + + // Method Description: + // - Tell the renderer to start painting. + // - !! IMPORTANT !! Make sure that we've attached our swap chain to an + // actual target before calling this. + // Arguments: + // - + // Return Value: + // - + void ControlCore::EnablePainting() + { + if (_initializedTerminal) + { + _renderer->EnablePainting(); + } + } + + // Method Description: + // - Writes the given sequence as input to the active terminal connection. + // - This method has been overloaded to allow zero-copy winrt::param::hstring optimizations. + // Arguments: + // - wstr: the string of characters to write to the terminal connection. + // Return Value: + // - + void ControlCore::_sendInputToConnection(std::wstring_view wstr) + { + if (_isReadOnly) + { + _raiseReadOnlyWarning(); + } + else + { + _connection.WriteInput(wstr); + } + } + + // Method Description: + // - Writes the given sequence as input to the active terminal connection, + // Arguments: + // - wstr: the string of characters to write to the terminal connection. + // Return Value: + // - + void ControlCore::SendInput(const winrt::hstring& wstr) + { + _sendInputToConnection(wstr); + } + + bool ControlCore::SendCharEvent(const wchar_t ch, + const WORD scanCode, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers) + { + return _terminal->SendCharEvent(ch, scanCode, modifiers); + } + + // 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. + // - scanCode: The scan code of the key pressed. + // - modifiers: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. + // - keyDown: If true, the key was pressed, otherwise the key was released. + bool ControlCore::TrySendKeyEvent(const WORD vkey, + const WORD scanCode, + const ControlKeyStates modifiers, + const bool keyDown) + { + // When there is a selection active, escape should clear it and NOT flow through + // to the terminal. With any other keypress, it should clear the selection AND + // flow through to the terminal. + // GH#6423 - don't dismiss selection if the key that was pressed was a + // modifier key. We'll wait for a real keystroke to dismiss the + // GH #7395 - don't dismiss selection when taking PrintScreen + // selection. + // GH#8522, GH#3758 - Only dismiss the selection on key _down_. If we + // dismiss on key up, then there's chance that we'll immediately dismiss + // a selection created by an action bound to a keydown. + if (HasSelection() && + !KeyEvent::IsModifierKey(vkey) && + vkey != VK_SNAPSHOT && + keyDown) + { + // GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination. + if (!modifiers.IsWinPressed()) + { + _terminal->ClearSelection(); + _renderer->TriggerSelection(); + } + + if (vkey == VK_ESCAPE) + { + return true; + } + } + + // 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 CharacterReceived event. + return vkey ? _terminal->SendKeyEvent(vkey, + scanCode, + modifiers, + keyDown) : + true; + } + + bool ControlCore::SendMouseEvent(const til::point viewportPos, + const unsigned int uiButton, + const ControlKeyStates states, + const short wheelDelta, + const TerminalInput::MouseButtonState state) + { + return _terminal->SendMouseEvent(viewportPos, uiButton, states, wheelDelta, state); + } + + void ControlCore::UserScrollViewport(const int viewTop) + { + // Clear the regex pattern tree so the renderer does not try to render them while scrolling + _terminal->ClearPatternTree(); + + // This is a scroll event that wasn't initiated by the terminal + // itself - it was initiated by the mouse wheel, or the scrollbar. + _terminal->UserScrollViewport(viewTop); + } + + void ControlCore::AdjustOpacity(const double adjustment) + { + if (adjustment == 0) + { + return; + } + + auto newOpacity = std::clamp(_settings.TintOpacity() + adjustment, + 0.0, + 1.0); + if (_settings.UseAcrylic()) + { + try + { + _settings.TintOpacity(newOpacity); + + if (newOpacity >= 1.0) + { + _settings.UseAcrylic(false); + } + else + { + // GH#5098: Inform the engine of the new opacity of the default text background. + SetBackgroundOpacity(::base::saturated_cast(newOpacity)); + } + + auto eventArgs = winrt::make_self(newOpacity); + _TransparencyChangedHandlers(*this, *eventArgs); + } + CATCH_LOG(); + } + else if (adjustment < 0) + { + _settings.UseAcrylic(true); + + //Setting initial opacity set to 1 to ensure smooth transition to acrylic during mouse scroll + newOpacity = std::clamp(1.0 + adjustment, 0.0, 1.0); + _settings.TintOpacity(newOpacity); + + auto eventArgs = winrt::make_self(newOpacity); + _TransparencyChangedHandlers(*this, *eventArgs); + } + } + + void ControlCore::ToggleShaderEffects() + { + auto lock = _terminal->LockForWriting(); + // Originally, this action could be used to enable the retro effects + // even when they're set to `false` in the settings. If the user didn't + // specify a custom pixel shader, manually enable the legacy retro + // effect first. This will ensure that a toggle off->on will still work, + // even if they currently have retro effect off. + if (_settings.PixelShaderPath().empty() && !_renderEngine->GetRetroTerminalEffect()) + { + // SetRetroTerminalEffect to true will enable the effect. In this + // case, the shader effect will already be disabled (because neither + // a pixel shader nor the retro effects were originally requested). + // So we _don't_ want to toggle it again below, because that would + // toggle it back off. + _renderEngine->SetRetroTerminalEffect(true); + } + else + { + _renderEngine->ToggleShaderEffects(); + } + } + + // Method Description: + // - Tell TerminalCore to update its knowledge about the locations of visible regex patterns + // - We should call this (through the throttled function) when something causes the visible + // region to change, such as when new text enters the buffer or the viewport is scrolled + void ControlCore::UpdatePatternLocations() + { + _terminal->UpdatePatterns(); + } + + // Method description: + // - Updates last hovered cell, renders / removes rendering of hyper-link if required + // Arguments: + // - terminalPosition: The terminal position of the pointer + void ControlCore::UpdateHoveredCell(const std::optional& terminalPosition) + { + if (terminalPosition == _lastHoveredCell) + { + return; + } + + // GH#9618 - lock while we're reading from the terminal, and if we need + // to update something, then lock again to write the terminal. + + _lastHoveredCell = terminalPosition; + uint16_t newId{ 0u }; + // we can't use auto here because we're pre-declaring newInterval. + decltype(_terminal->GetHyperlinkIntervalFromPosition(til::point{})) newInterval{ std::nullopt }; + if (terminalPosition.has_value()) + { + auto lock = _terminal->LockForReading(); // Lock for the duration of our reads. + newId = _terminal->GetHyperlinkIdAtPosition(*terminalPosition); + newInterval = _terminal->GetHyperlinkIntervalFromPosition(*terminalPosition); + } + + // If the hyperlink ID changed or the interval changed, trigger a redraw all + // (so this will happen both when we move onto a link and when we move off a link) + if (newId != _lastHoveredId || + (newInterval != _lastHoveredInterval)) + { + // Introduce scope for lock - we don't want to raise the + // HoveredHyperlinkChanged event under lock, because then handlers + // wouldn't be able to ask us about the hyperlink text/position + // without deadlocking us. + { + auto lock = _terminal->LockForWriting(); + + _lastHoveredId = newId; + _lastHoveredInterval = newInterval; + _renderEngine->UpdateHyperlinkHoveredId(newId); + _renderer->UpdateLastHoveredInterval(newInterval); + _renderer->TriggerRedrawAll(); + } + + _HoveredHyperlinkChangedHandlers(*this, nullptr); + } + } + + winrt::hstring ControlCore::GetHyperlink(const til::point pos) const + { + // Lock for the duration of our reads. + auto lock = _terminal->LockForReading(); + return winrt::hstring{ _terminal->GetHyperlinkAtPosition(pos) }; + } + + winrt::hstring ControlCore::GetHoveredUriText() const + { + auto lock = _terminal->LockForReading(); // Lock for the duration of our reads. + if (_lastHoveredCell.has_value()) + { + return winrt::hstring{ _terminal->GetHyperlinkAtPosition(*_lastHoveredCell) }; + } + return {}; + } + + std::optional ControlCore::GetHoveredCell() const + { + return _lastHoveredCell; + } + + // Method Description: + // - Updates the settings of the current terminal. + // - INVARIANT: This method can only be called if the caller DOES NOT HAVE writing lock on the terminal. + void ControlCore::UpdateSettings(const IControlSettings& settings) + { + auto lock = _terminal->LockForWriting(); + + _settings = settings; + + // Initialize our font information. + const auto fontFace = _settings.FontFace(); + const short fontHeight = ::base::saturated_cast(_settings.FontSize()); + const auto fontWeight = _settings.FontWeight(); + // 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, fontWeight.Weight, { 0, fontHeight }, CP_UTF8, false }; + _desiredFont = { _actualFont }; + + // Update the terminal core with its new Core settings + _terminal->UpdateSettings(_settings); + + if (!_initializedTerminal) + { + // If we haven't initialized, there's no point in continuing. + // Initialization will handle the renderer settings. + return; + } + + _renderEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); + _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering()); + _updateAntiAliasingMode(_renderEngine.get()); + + // Refresh our font with the renderer + const auto actualFontOldSize = _actualFont.GetSize(); + _updateFont(); + const auto actualFontNewSize = _actualFont.GetSize(); + if (actualFontNewSize != actualFontOldSize) + { + _refreshSizeUnderLock(); + } + } + + // Method Description: + // - Updates the appearance of the current terminal. + // - INVARIANT: This method can only be called if the caller DOES NOT HAVE writing lock on the terminal. + void ControlCore::UpdateAppearance(const IControlAppearance& newAppearance) + { + auto lock = _terminal->LockForWriting(); + + // Update the terminal core with its new Core settings + _terminal->UpdateAppearance(newAppearance); + + // Update DxEngine settings under the lock + if (_renderEngine) + { + // Update DxEngine settings under the lock + _renderEngine->SetSelectionBackground(til::color{ newAppearance.SelectionBackground() }); + _renderEngine->SetRetroTerminalEffect(newAppearance.RetroTerminalEffect()); + _renderEngine->SetPixelShaderPath(newAppearance.PixelShaderPath()); + _renderer->TriggerRedrawAll(); + } + } + + void ControlCore::_updateAntiAliasingMode(::Microsoft::Console::Render::DxEngine* const dxEngine) + { + // Update DxEngine's AntialiasingMode + switch (_settings.AntialiasingMode()) + { + case TextAntialiasingMode::Cleartype: + dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE); + break; + case TextAntialiasingMode::Aliased: + dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_ALIASED); + break; + case TextAntialiasingMode::Grayscale: + default: + dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); + break; + } + } + + // 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 _doResizeUnderLock after this method is called. + // - The write lock should be held when calling this method. + // Arguments: + // - initialUpdate: whether this font update should be considered as being + // concerned with initialization process. Value forwarded to event handler. + void ControlCore::_updateFont(const bool initialUpdate) + { + const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * + _compositionScale); + + // 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); + + // If the actual font isn't what was requested... + if (_actualFont.GetFaceName() != _desiredFont.GetFaceName()) + { + // Then warn the user that we picked something because we couldn't find their font. + // Format message with user's choice of font and the font that was chosen instead. + const winrt::hstring message{ fmt::format(std::wstring_view{ RS_(L"NoticeFontNotFound") }, + _desiredFont.GetFaceName(), + _actualFont.GetFaceName()) }; + auto noticeArgs = winrt::make(NoticeLevel::Warning, message); + _RaiseNoticeHandlers(*this, std::move(noticeArgs)); + } + + const auto actualNewSize = _actualFont.GetSize(); + _FontSizeChangedHandlers(actualNewSize.X, actualNewSize.Y, initialUpdate); + } + + // Method Description: + // - Set the font size of the terminal control. + // Arguments: + // - fontSize: The size of the font. + void ControlCore::_setFontSize(int fontSize) + { + try + { + // Make sure we have a non-zero font size + const auto newSize = std::max(gsl::narrow_cast(fontSize), 1); + const auto fontFace = _settings.FontFace(); + const auto fontWeight = _settings.FontWeight(); + _actualFont = { fontFace, 0, fontWeight.Weight, { 0, newSize }, CP_UTF8, false }; + _desiredFont = { _actualFont }; + + auto lock = _terminal->LockForWriting(); + + // 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?) + _refreshSizeUnderLock(); + } + CATCH_LOG(); + } + + // Method Description: + // - Reset the font size of the terminal to its default size. + // Arguments: + // - none + void ControlCore::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 ControlCore::AdjustFontSize(int fontSizeDelta) + { + const auto newSize = _desiredFont.GetEngineSize().Y + fontSizeDelta; + _setFontSize(newSize); + } + + // Method Description: + // - Perform a resize for the current size of the swapchainpanel. If the + // font size changed, we'll need to resize the buffer to fit the existing + // swapchain size. This helper will call _doResizeUnderLock with the + // current size of the swapchain, accounting for scaling due to DPI. + // - Note that a DPI change will also trigger a font size change, and will + // call into here. + // - The write lock should be held when calling this method, we might be + // changing the buffer size in _doResizeUnderLock. + // Arguments: + // - + // Return Value: + // - + void ControlCore::_refreshSizeUnderLock() + { + const auto widthInPixels = _panelWidth * _compositionScale; + const auto heightInPixels = _panelHeight * _compositionScale; + + _doResizeUnderLock(widthInPixels, heightInPixels); + } + + // 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 ControlCore::_doResizeUnderLock(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; + } + + _terminal->ClearSelection(); + + // 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. + const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() }); + if (SUCCEEDED(hr) && hr != S_FALSE) + { + _connection.Resize(vp.Height(), vp.Width()); + } + } + + void ControlCore::SizeChanged(const double width, + const double height) + { + _panelWidth = width; + _panelHeight = height; + + auto lock = _terminal->LockForWriting(); + const auto currentEngineScale = _renderEngine->GetScaling(); + + auto scaledWidth = width * currentEngineScale; + auto scaledHeight = height * currentEngineScale; + _doResizeUnderLock(scaledWidth, scaledHeight); + } + + void ControlCore::ScaleChanged(const double scale) + { + if (!_renderEngine) + { + return; + } + + const auto currentEngineScale = _renderEngine->GetScaling(); + // If we're getting a notification to change to the DPI we already + // have, then we're probably just beginning the DPI change. Since + // we'll get _another_ event with the real DPI, do nothing here for + // now. We'll also skip the next resize in _swapChainSizeChanged. + const bool dpiWasUnchanged = currentEngineScale == scale; + if (dpiWasUnchanged) + { + return; + } + + const auto dpi = (float)(scale * USER_DEFAULT_SCREEN_DPI); + + const auto actualFontOldSize = _actualFont.GetSize(); + + auto lock = _terminal->LockForWriting(); + _compositionScale = scale; + + _renderer->TriggerFontChange(::base::saturated_cast(dpi), + _desiredFont, + _actualFont); + + const auto actualFontNewSize = _actualFont.GetSize(); + if (actualFontNewSize != actualFontOldSize) + { + _refreshSizeUnderLock(); + } + } + + void ControlCore::SetSelectionAnchor(til::point const& position) + { + auto lock = _terminal->LockForWriting(); + _terminal->SetSelectionAnchor(position); + } + + // Method Description: + // - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging. + // Arguments: + // - position: the point in terminal coordinates (in cells, not pixels) + void ControlCore::SetEndSelectionPoint(til::point const& position) + { + if (!_terminal->IsSelectionActive()) + { + return; + } + + // Have to take the lock because the renderer will not draw correctly if + // you move its endpoints while it is generating a frame. + auto lock = _terminal->LockForWriting(); + + const short lastVisibleRow = std::max(_terminal->GetViewport().Height() - 1, 0); + const short lastVisibleCol = std::max(_terminal->GetViewport().Width() - 1, 0); + + til::point terminalPosition{ + std::clamp(position.x(), 0, lastVisibleCol), + std::clamp(position.y(), 0, lastVisibleRow) + }; + + // save location (for rendering) + render + _terminal->SetSelectionEnd(terminalPosition); + _renderer->TriggerSelection(); + } + + // Called when the Terminal wants to set something to the clipboard, i.e. + // when an OSC 52 is emitted. + void ControlCore::_terminalCopyToClipboard(std::wstring_view wstr) + { + _CopyToClipboardHandlers(*this, winrt::make(winrt::hstring{ wstr })); + } + + // 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: + // - singleLine: collapse all of the text to one line + // - formats: which formats to copy (defined by action's CopyFormatting arg). nullptr + // if we should defer which formats are copied to the global setting + bool ControlCore::CopySelectionToClipboard(bool singleLine, + const Windows::Foundation::IReference& formats) + { + // no selection --> nothing to copy + if (!_terminal->IsSelectionActive()) + { + return false; + } + + // extract text from buffer + // RetrieveSelectedTextFromBuffer will lock while it's reading + const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(singleLine); + + // convert text: vector --> string + std::wstring textData; + for (const auto& text : bufferData.text) + { + textData += text; + } + + // convert text to HTML format + // GH#5347 - Don't provide a title for the generated HTML, as many + // web applications will paste the title first, followed by the HTML + // content, which is unexpected. + const auto htmlData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::HTML) ? + TextBuffer::GenHTML(bufferData, + _actualFont.GetUnscaledSize().Y, + _actualFont.GetFaceName(), + til::color{ _settings.DefaultBackground() }) : + ""; + + // convert to RTF format + const auto rtfData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::RTF) ? + TextBuffer::GenRTF(bufferData, + _actualFont.GetUnscaledSize().Y, + _actualFont.GetFaceName(), + til::color{ _settings.DefaultBackground() }) : + ""; + + if (!_settings.CopyOnSelect()) + { + _terminal->ClearSelection(); + _renderer->TriggerSelection(); + } + + // send data up for clipboard + _CopyToClipboardHandlers(*this, + winrt::make(winrt::hstring{ textData }, + winrt::to_hstring(htmlData), + winrt::to_hstring(rtfData), + formats)); + return true; + } + + // Method Description: + // - Pre-process text pasted (presumably from the clipboard) + // before sending it over the terminal's connection. + void ControlCore::PasteText(const winrt::hstring& hstr) + { + _terminal->WritePastedText(hstr); + _terminal->ClearSelection(); + _terminal->TrySnapOnInput(); + } + + FontInfo ControlCore::GetFont() const + { + return _actualFont; + } + + til::size ControlCore::FontSizeInDips() const + { + const til::size fontSize{ GetFont().GetSize() }; + return fontSize.scale(til::math::rounding, 1.0f / ::base::saturated_cast(_compositionScale)); + } + + TerminalConnection::ConnectionState ControlCore::ConnectionState() const + { + return _connection.State(); + } + + hstring ControlCore::Title() + { + return hstring{ _terminal->GetConsoleTitle() }; + } + + hstring ControlCore::WorkingDirectory() const + { + return hstring{ _terminal->GetWorkingDirectory() }; + } + + bool ControlCore::BracketedPasteEnabled() const noexcept + { + return _terminal->IsXtermBracketedPasteModeEnabled(); + } + + Windows::Foundation::IReference ControlCore::TabColor() noexcept + { + auto coreColor = _terminal->GetTabColor(); + return coreColor.has_value() ? Windows::Foundation::IReference(til::color{ coreColor.value() }) : + nullptr; + } + + til::color ControlCore::BackgroundColor() const + { + return _terminal->GetDefaultBackground(); + } + + // Method Description: + // - Gets the internal taskbar state value + // Return Value: + // - The taskbar state of this control + const size_t ControlCore::TaskbarState() const noexcept + { + return _terminal->GetTaskbarState(); + } + + // Method Description: + // - Gets the internal taskbar progress value + // Return Value: + // - The taskbar progress of this control + const size_t ControlCore::TaskbarProgress() const noexcept + { + return _terminal->GetTaskbarProgress(); + } + + int ControlCore::ScrollOffset() + { + return _terminal->GetScrollOffset(); + } + + // Function Description: + // - Gets the height of the terminal in lines of text. This is just the + // height of the viewport. + // Return Value: + // - The height of the terminal in lines of text + int ControlCore::ViewHeight() const + { + return _terminal->GetViewport().Height(); + } + + // Function Description: + // - Gets the height of the terminal in lines of text. This includes the + // history AND the viewport. + // Return Value: + // - The height of the terminal in lines of text + int ControlCore::BufferHeight() const + { + return _terminal->GetBufferHeight(); + } + + void ControlCore::_terminalWarningBell() + { + // Since this can only ever be triggered by output from the connection, + // then the Terminal already has the write lock when calling this + // callback. + _WarningBellHandlers(*this, nullptr); + } + + // Method Description: + // - Called for the Terminal's TitleChanged callback. This will re-raise + // a new winrt TypedEvent that can be listened to. + // - The listeners to this event will re-query the control for the current + // value of Title(). + // Arguments: + // - wstr: the new title of this terminal. + // Return Value: + // - + void ControlCore::_terminalTitleChanged(std::wstring_view wstr) + { + // Since this can only ever be triggered by output from the connection, + // then the Terminal already has the write lock when calling this + // callback. + _TitleChangedHandlers(*this, winrt::make(winrt::hstring{ wstr })); + } + + // Method Description: + // - Called for the Terminal's TabColorChanged callback. This will re-raise + // a new winrt TypedEvent that can be listened to. + // - The listeners to this event will re-query the control for the current + // value of TabColor(). + // Arguments: + // - + // Return Value: + // - + void ControlCore::_terminalTabColorChanged(const std::optional /*color*/) + { + // Raise a TabColorChanged event + _TabColorChangedHandlers(*this, nullptr); + } + + // Method Description: + // - Called for the Terminal's BackgroundColorChanged callback. This will + // re-raise a new winrt TypedEvent that can be listened to. + // - The listeners to this event will re-query the control for the current + // value of BackgroundColor(). + // Arguments: + // - + // Return Value: + // - + void ControlCore::_terminalBackgroundColorChanged(const COLORREF /*color*/) + { + // Raise a BackgroundColorChanged event + _BackgroundColorChangedHandlers(*this, nullptr); + } + + // Method Description: + // - Update the position 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 ControlCore::_terminalScrollPositionChanged(const int viewTop, + const int viewHeight, + const int bufferSize) + { + // Clear the regex pattern tree so the renderer does not try to render them while scrolling + // We're **NOT** taking the lock here unlike _scrollbarChangeHandler because + // we are already under lock (since this usually happens as a result of writing). + // TODO GH#9617: refine locking around pattern tree + _terminal->ClearPatternTree(); + + _ScrollPositionChangedHandlers(*this, + winrt::make(viewTop, + viewHeight, + bufferSize)); + } + + void ControlCore::_terminalCursorPositionChanged() + { + _CursorPositionChangedHandlers(*this, nullptr); + } + + void ControlCore::_terminalTaskbarProgressChanged() + { + _TaskbarProgressChangedHandlers(*this, nullptr); + } + + bool ControlCore::HasSelection() const + { + return _terminal->IsSelectionActive(); + } + + bool ControlCore::CopyOnSelect() const + { + return _settings.CopyOnSelect(); + } + + std::vector ControlCore::SelectedText(bool trimTrailingWhitespace) const + { + // RetrieveSelectedTextFromBuffer will lock while it's reading + return _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace).text; + } + + ::Microsoft::Console::Types::IUiaData* ControlCore::GetUiaData() const + { + return _terminal.get(); + } + + // Method Description: + // - Search text in text buffer. This is triggered if the user click + // search button or press enter. + // Arguments: + // - text: the text to search + // - goForward: boolean that represents if the current search direction is forward + // - caseSensitive: boolean that represents if the current search is case sensitive + // Return Value: + // - + void ControlCore::Search(const winrt::hstring& text, + const bool goForward, + const bool caseSensitive) + { + if (text.size() == 0) + { + return; + } + + const Search::Direction direction = goForward ? + Search::Direction::Forward : + Search::Direction::Backward; + + const Search::Sensitivity sensitivity = caseSensitive ? + Search::Sensitivity::CaseSensitive : + Search::Sensitivity::CaseInsensitive; + + ::Search search(*GetUiaData(), text.c_str(), direction, sensitivity); + auto lock = _terminal->LockForWriting(); + if (search.FindNext()) + { + _terminal->SetBlockSelection(false); + search.Select(); + _renderer->TriggerSelection(); + } + } + + void ControlCore::SetBackgroundOpacity(const float opacity) + { + if (_renderEngine) + { + _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(opacity)); + } + } + + // Method Description: + // - Asynchronously close our connection. The Connection will likely wait + // until the attached process terminates before Close returns. If that's + // the case, we don't want to block the UI thread waiting on that process + // handle. + // Arguments: + // - + // Return Value: + // - + winrt::fire_and_forget ControlCore::_asyncCloseConnection() + { + if (auto localConnection{ std::exchange(_connection, nullptr) }) + { + // Close the connection on the background thread. + co_await winrt::resume_background(); // ** DO NOT INTERACT WITH THE CONTROL CORE AFTER THIS LINE ** + + // Here, the ControlCore very well might be gone. + // _asyncCloseConnection is called on the dtor, so it's entirely + // possible that the background thread is resuming after we've been + // cleaned up. + + localConnection.Close(); + // connection is destroyed. + } + } + + void ControlCore::Close() + { + if (!_closing.exchange(true)) + { + // Stop accepting new output and state changes before we disconnect everything. + _connection.TerminalOutput(_connectionOutputEventToken); + _connectionStateChangedRevoker.revoke(); + + // GH#1996 - Close the connection asynchronously on a background + // thread. + // Since TermControl::Close is only ever triggered by the UI, we + // don't really care to wait for the connection to be completely + // closed. We can just do it whenever. + _asyncCloseConnection(); + + { + // GH#8734: + // We lock the terminal here to make sure it isn't still being + // used in the connection thread before we destroy the renderer. + // However, we must unlock it again prior to triggering the + // teardown, to avoid the render thread being deadlocked. The + // renderer may be waiting to acquire the terminal lock, while + // we're waiting for the renderer to finish. + auto lock = _terminal->LockForWriting(); + } + + if (auto localRenderEngine{ std::exchange(_renderEngine, nullptr) }) + { + if (auto localRenderer{ std::exchange(_renderer, nullptr) }) + { + localRenderer->TriggerTeardown(); + // renderer is destroyed + } + // renderEngine is destroyed + } + + // we don't destroy _terminal here; it now has the same lifetime as the + // control. + } + } + + IDXGISwapChain1* ControlCore::GetSwapChain() const + { + // This is called by: + // * TermControl::RenderEngineSwapChainChanged, who is only registered + // after Core::Initialize() is called. + // * TermControl::_InitializeTerminal, after the call to Initialize, for + // _AttachDxgiSwapChainToXaml. + // In both cases, we'll have a _renderEngine by then. + return _renderEngine->GetSwapChain().Get(); + } + + void ControlCore::_rendererWarning(const HRESULT hr) + { + _RendererWarningHandlers(*this, winrt::make(hr)); + } + + void ControlCore::_renderEngineSwapChainChanged() + { + _SwapChainChangedHandlers(*this, nullptr); + } + + void ControlCore::BlinkAttributeTick() + { + auto lock = _terminal->LockForWriting(); + + auto& renderTarget = *_renderer; + auto& blinkingState = _terminal->GetBlinkingState(); + blinkingState.ToggleBlinkingRendition(renderTarget); + } + + void ControlCore::BlinkCursor() + { + if (!_terminal->IsCursorBlinkingAllowed() && + _terminal->IsCursorVisible()) + { + return; + } + // SetCursorOn will take the write lock for you. + _terminal->SetCursorOn(!_terminal->IsCursorOn()); + } + + bool ControlCore::CursorOn() const + { + return _terminal->IsCursorOn(); + } + + void ControlCore::CursorOn(const bool isCursorOn) + { + _terminal->SetCursorOn(isCursorOn); + } + + void ControlCore::ResumeRendering() + { + _renderer->ResetErrorStateAndResume(); + } + + bool ControlCore::IsVtMouseModeEnabled() const + { + return _terminal != nullptr && _terminal->IsTrackingMouseInput(); + } + + til::point ControlCore::CursorPosition() const + { + // If we haven't been initialized yet, then fake it. + if (!_initializedTerminal) + { + return { 0, 0 }; + } + + auto lock = _terminal->LockForReading(); + return _terminal->GetCursorPosition(); + } + + // This one's really pushing the boundary of what counts as "encapsulation". + // It really belongs in the "Interactivity" layer, which doesn't yet exist. + // There's so many accesses to the selection in the Core though, that I just + // put this here. The Control shouldn't be futzing that much with the + // selection itself. + void ControlCore::LeftClickOnTerminal(const til::point terminalPosition, + const int numberOfClicks, + const bool altEnabled, + const bool shiftEnabled, + const bool isOnOriginalPosition, + bool& selectionNeedsToBeCopied) + { + auto lock = _terminal->LockForWriting(); + // handle ALT key + _terminal->SetBlockSelection(altEnabled); + + ::Terminal::SelectionExpansionMode mode = ::Terminal::SelectionExpansionMode::Cell; + if (numberOfClicks == 1) + { + mode = ::Terminal::SelectionExpansionMode::Cell; + } + else if (numberOfClicks == 2) + { + mode = ::Terminal::SelectionExpansionMode::Word; + } + else if (numberOfClicks == 3) + { + mode = ::Terminal::SelectionExpansionMode::Line; + } + + // Update the selection appropriately + + // We reset the active selection if one of the conditions apply: + // - shift is not held + // - GH#9384: the position is the same as of the first click starting + // the selection (we need to reset selection on double-click or + // triple-click, so it captures the word or the line, rather than + // extending the selection) + if (HasSelection() && (!shiftEnabled || isOnOriginalPosition)) + { + // Reset the selection + _terminal->ClearSelection(); + selectionNeedsToBeCopied = false; // there's no selection, so there's nothing to update + } + + if (shiftEnabled && HasSelection()) + { + // If shift is pressed and there is a selection we extend it using + // the selection mode (expand the "end" selection point) + _terminal->SetSelectionEnd(terminalPosition, mode); + selectionNeedsToBeCopied = true; + } + else if (mode != ::Terminal::SelectionExpansionMode::Cell || shiftEnabled) + { + // If we are handling a double / triple-click or shift+single click + // we establish selection using the selected mode + // (expand both "start" and "end" selection points) + _terminal->MultiClickSelection(terminalPosition, mode); + selectionNeedsToBeCopied = true; + } + + _renderer->TriggerSelection(); + } + + void ControlCore::AttachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine) + { + if (_renderer) + { + _renderer->AddRenderEngine(pEngine); + } + } + + bool ControlCore::IsInReadOnlyMode() const + { + return _isReadOnly; + } + + void ControlCore::ToggleReadOnlyMode() + { + _isReadOnly = !_isReadOnly; + } + + void ControlCore::_raiseReadOnlyWarning() + { + auto noticeArgs = winrt::make(NoticeLevel::Info, RS_(L"TermControlReadOnly")); + _RaiseNoticeHandlers(*this, std::move(noticeArgs)); + } + void ControlCore::_connectionOutputHandler(const hstring& hstr) + { + _terminal->Write(hstr); + + // NOTE: We're raising an event here to inform the TermControl that + // output has been received, so it can queue up a throttled + // UpdatePatternLocations call. In the future, we should have the + // _updatePatternLocations ThrottledFunc internal to this class, and + // run on this object's dispatcher queue. + // + // We're not doing that quite yet, because the Core will eventually + // be out-of-proc from the UI thread, and won't be able to just use + // the UI thread as the dispatcher queue thread. + // + // See TODO: https://github.com/microsoft/terminal/projects/5#card-50760282 + _ReceivedOutputHandlers(*this, nullptr); + } + +} diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h new file mode 100644 index 00000000000..8ab58a2243d --- /dev/null +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// Module Name: +// - ControlCore.h +// +// Abstract: +// - This encapsulates a `Terminal` instance, a `DxEngine` and `Renderer`, and +// an `ITerminalConnection`. This is intended to be everything that someone +// might need to stand up a terminal instance in a control, but without any +// regard for how the UX works. +// +// Author: +// - Mike Griese (zadjii-msft) 01-Apr-2021 + +#pragma once + +#include "EventArgs.h" +#include "ControlCore.g.h" +#include "../../renderer/base/Renderer.hpp" +#include "../../renderer/dx/DxRenderer.hpp" +#include "../../renderer/uia/UiaRenderer.hpp" +#include "../../cascadia/TerminalCore/Terminal.hpp" +#include "../buffer/out/search.h" +#include "cppwinrt_utils.h" +#include "ThrottledFunc.h" + +namespace ControlUnitTests +{ + class ControlCoreTests; + class ControlInteractivityTests; +}; + +namespace winrt::Microsoft::Terminal::Control::implementation +{ + struct ControlCore : ControlCoreT + { + public: + ControlCore(IControlSettings settings, + TerminalConnection::ITerminalConnection connection); + ~ControlCore(); + + bool Initialize(const double actualWidth, + const double actualHeight, + const double compositionScale); + void EnablePainting(); + + void UpdateSettings(const IControlSettings& settings); + void UpdateAppearance(const IControlAppearance& newAppearance); + void SizeChanged(const double width, const double height); + void ScaleChanged(const double scale); + IDXGISwapChain1* GetSwapChain() const; + + void AdjustFontSize(int fontSizeDelta); + void ResetFontSize(); + FontInfo GetFont() const; + til::size FontSizeInDips() const; + + til::color BackgroundColor() const; + void SetBackgroundOpacity(const float opacity); + + void SendInput(const winrt::hstring& wstr); + void PasteText(const winrt::hstring& hstr); + bool CopySelectionToClipboard(bool singleLine, const Windows::Foundation::IReference& formats); + + void ToggleShaderEffects(); + void AdjustOpacity(const double adjustment); + void ResumeRendering(); + + void UpdatePatternLocations(); + void UpdateHoveredCell(const std::optional& terminalPosition); + winrt::hstring GetHyperlink(const til::point position) const; + winrt::hstring GetHoveredUriText() const; + std::optional GetHoveredCell() const; + + ::Microsoft::Console::Types::IUiaData* GetUiaData() const; + + void Close(); + +#pragma region ICoreState + const size_t TaskbarState() const noexcept; + const size_t TaskbarProgress() const noexcept; + + hstring Title(); + Windows::Foundation::IReference TabColor() noexcept; + hstring WorkingDirectory() const; + + TerminalConnection::ConnectionState ConnectionState() const; + + int ScrollOffset(); + int ViewHeight() const; + int BufferHeight() const; + + bool BracketedPasteEnabled() const noexcept; +#pragma endregion + +#pragma region ITerminalInput + bool TrySendKeyEvent(const WORD vkey, + const WORD scanCode, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const bool keyDown); + bool SendCharEvent(const wchar_t ch, + const WORD scanCode, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers); + bool SendMouseEvent(const til::point viewportPos, + const unsigned int uiButton, + const ::Microsoft::Terminal::Core::ControlKeyStates states, + const short wheelDelta, + const ::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state); + void UserScrollViewport(const int viewTop); +#pragma endregion + + void BlinkAttributeTick(); + void BlinkCursor(); + bool CursorOn() const; + void CursorOn(const bool isCursorOn); + + bool IsVtMouseModeEnabled() const; + til::point CursorPosition() const; + + bool HasSelection() const; + bool CopyOnSelect() const; + std::vector SelectedText(bool trimTrailingWhitespace) const; + void SetSelectionAnchor(til::point const& position); + void SetEndSelectionPoint(til::point const& position); + + void Search(const winrt::hstring& text, + const bool goForward, + const bool caseSensitive); + + void LeftClickOnTerminal(const til::point terminalPosition, + const int numberOfClicks, + const bool altEnabled, + const bool shiftEnabled, + const bool isOnOriginalPosition, + bool& selectionNeedsToBeCopied); + + void AttachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine); + + bool IsInReadOnlyMode() const; + void ToggleReadOnlyMode(); + + // -------------------------------- WinRT Events --------------------------------- + // clang-format off + WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); + + TYPED_EVENT(CopyToClipboard, IInspectable, Control::CopyToClipboardEventArgs); + TYPED_EVENT(TitleChanged, IInspectable, Control::TitleChangedEventArgs); + TYPED_EVENT(WarningBell, IInspectable, IInspectable); + TYPED_EVENT(TabColorChanged, IInspectable, IInspectable); + TYPED_EVENT(BackgroundColorChanged, IInspectable, IInspectable); + TYPED_EVENT(ScrollPositionChanged, IInspectable, Control::ScrollPositionChangedArgs); + TYPED_EVENT(CursorPositionChanged, IInspectable, IInspectable); + TYPED_EVENT(TaskbarProgressChanged, IInspectable, IInspectable); + TYPED_EVENT(ConnectionStateChanged, IInspectable, IInspectable); + TYPED_EVENT(HoveredHyperlinkChanged, IInspectable, IInspectable); + TYPED_EVENT(RendererEnteredErrorState, IInspectable, IInspectable); + TYPED_EVENT(SwapChainChanged, IInspectable, IInspectable); + TYPED_EVENT(RendererWarning, IInspectable, Control::RendererWarningArgs); + TYPED_EVENT(RaiseNotice, IInspectable, Control::NoticeEventArgs); + TYPED_EVENT(TransparencyChanged, IInspectable, Control::TransparencyChangedEventArgs); + TYPED_EVENT(ReceivedOutput, IInspectable, IInspectable); + // clang-format on + + private: + bool _initializedTerminal{ false }; + std::atomic _closing{ false }; + + TerminalConnection::ITerminalConnection _connection{ nullptr }; + event_token _connectionOutputEventToken; + TerminalConnection::ITerminalConnection::StateChanged_revoker _connectionStateChangedRevoker; + + std::unique_ptr<::Microsoft::Terminal::Core::Terminal> _terminal{ nullptr }; + + std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer{ nullptr }; + std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine{ nullptr }; + + IControlSettings _settings{ nullptr }; + + FontInfoDesired _desiredFont; + FontInfo _actualFont; + + // storage location for the leading surrogate of a utf-16 surrogate pair + std::optional _leadingSurrogate{ std::nullopt }; + + std::optional _lastHoveredCell{ std::nullopt }; + // Track the last hyperlink ID we hovered over + uint16_t _lastHoveredId{ 0 }; + + bool _isReadOnly{ false }; + + std::optional::interval> _lastHoveredInterval{ std::nullopt }; + + // These members represent the size of the surface that we should be + // rendering to. + double _panelWidth{ 0 }; + double _panelHeight{ 0 }; + double _compositionScale{ 0 }; + + winrt::fire_and_forget _asyncCloseConnection(); + + void _setFontSize(int fontSize); + void _updateFont(const bool initialUpdate = false); + void _refreshSizeUnderLock(); + void _doResizeUnderLock(const double newWidth, + const double newHeight); + + void _sendInputToConnection(std::wstring_view wstr); + +#pragma region TerminalCoreCallbacks + void _terminalCopyToClipboard(std::wstring_view wstr); + void _terminalWarningBell(); + void _terminalTitleChanged(std::wstring_view wstr); + void _terminalTabColorChanged(const std::optional color); + void _terminalBackgroundColorChanged(const COLORREF color); + void _terminalScrollPositionChanged(const int viewTop, + const int viewHeight, + const int bufferSize); + void _terminalCursorPositionChanged(); + void _terminalTaskbarProgressChanged(); +#pragma endregion + +#pragma region RendererCallbacks + void _rendererWarning(const HRESULT hr); + void _renderEngineSwapChainChanged(); +#pragma endregion + + void _raiseReadOnlyWarning(); + void _updateAntiAliasingMode(::Microsoft::Console::Render::DxEngine* const dxEngine); + void _connectionOutputHandler(const hstring& hstr); + + friend class ControlUnitTests::ControlCoreTests; + friend class ControlUnitTests::ControlInteractivityTests; + }; +} + +namespace winrt::Microsoft::Terminal::Control::factory_implementation +{ + BASIC_FACTORY(ControlCore); +} diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl new file mode 100644 index 00000000000..c3074000f75 --- /dev/null +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ICoreState.idl"; +import "IControlSettings.idl"; +import "EventArgs.idl"; + +namespace Microsoft.Terminal.Control +{ + + [default_interface] runtimeclass ControlCore : ICoreState + { + ControlCore(IControlSettings settings, + Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); + }; +} diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp new file mode 100644 index 00000000000..ecee7cc0e56 --- /dev/null +++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp @@ -0,0 +1,531 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "ControlInteractivity.h" +#include +#include +#include +#include +#include +#include +#include +#include "../../types/inc/GlyphWidth.hpp" +#include "../../types/inc/Utils.hpp" +#include "../../buffer/out/search.h" + +#include "ControlInteractivity.g.cpp" + +using namespace ::Microsoft::Console::Types; +using namespace ::Microsoft::Console::VirtualTerminal; +using namespace ::Microsoft::Terminal::Core; +using namespace winrt::Windows::Graphics::Display; +using namespace winrt::Windows::System; +using namespace winrt::Windows::ApplicationModel::DataTransfer; + +static constexpr unsigned int MAX_CLICK_COUNT = 3; + +namespace winrt::Microsoft::Terminal::Control::implementation +{ + ControlInteractivity::ControlInteractivity(IControlSettings settings, + TerminalConnection::ITerminalConnection connection) : + _touchAnchor{ std::nullopt }, + _lastMouseClickTimestamp{}, + _lastMouseClickPos{}, + _selectionNeedsToBeCopied{ false } + { + _core = winrt::make_self(settings, connection); + } + + void ControlInteractivity::UpdateSettings() + { + _updateSystemParameterSettings(); + } + + void ControlInteractivity::Initialize() + { + _updateSystemParameterSettings(); + + // import value from WinUser (convert from milli-seconds to micro-seconds) + _multiClickTimer = GetDoubleClickTime() * 1000; + } + + winrt::com_ptr ControlInteractivity::GetCore() + { + return _core; + } + + // Method Description: + // - Returns the number of clicks that occurred (double and triple click support). + // Every call to this function registers a click. + // 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 + unsigned int ControlInteractivity::_numberOfClicks(til::point clickPos, + Timestamp clickTime) + { + // if click occurred at a different location or past the multiClickTimer... + Timestamp delta; + THROW_IF_FAILED(UInt64Sub(clickTime, _lastMouseClickTimestamp, &delta)); + if (clickPos != _lastMouseClickPos || delta > _multiClickTimer) + { + _multiClickCounter = 1; + } + else + { + _multiClickCounter++; + } + + _lastMouseClickTimestamp = clickTime; + _lastMouseClickPos = clickPos; + return _multiClickCounter; + } + + void ControlInteractivity::GainFocus() + { + _updateSystemParameterSettings(); + } + + // Method Description + // - Updates internal params based on system parameters + void ControlInteractivity::_updateSystemParameterSettings() noexcept + { + if (!SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &_rowsToScroll, 0)) + { + LOG_LAST_ERROR(); + // If SystemParametersInfoW fails, which it shouldn't, fall back to + // Windows' default value. + _rowsToScroll = 3; + } + } + + // 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: + // - singleLine: collapse all of the text to one line + // - formats: which formats to copy (defined by action's CopyFormatting arg). nullptr + // if we should defer which formats are copied to the global setting + bool ControlInteractivity::CopySelectionToClipboard(bool singleLine, + const Windows::Foundation::IReference& formats) + { + if (_core) + { + // Return false if there's no selection to copy. If there's no + // selection, returning false will indicate that the actions that + // triggered this should _not_ be marked as handled, so ctrl+c + // without a selection can still send ^C + if (!_core->HasSelection()) + { + return false; + } + + // Mark the current selection as copied + _selectionNeedsToBeCopied = false; + + return _core->CopySelectionToClipboard(singleLine, formats); + } + + return false; + } + + // Method Description: + // - Initiate a paste operation. + void ControlInteractivity::RequestPasteTextFromClipboard() + { + // attach ControlInteractivity::_sendPastedTextToConnection() as the + // clipboardDataHandler. This is called when the clipboard data is + // loaded. + auto clipboardDataHandler = std::bind(&ControlInteractivity::_sendPastedTextToConnection, this, std::placeholders::_1); + auto pasteArgs = winrt::make_self(clipboardDataHandler); + + // send paste event up to TermApp + _PasteFromClipboardHandlers(*this, *pasteArgs); + } + + // Method Description: + // - Pre-process text pasted (presumably from the clipboard) + // before sending it over the terminal's connection. + void ControlInteractivity::_sendPastedTextToConnection(std::wstring_view wstr) + { + _core->PasteText(winrt::hstring{ wstr }); + } + + void ControlInteractivity::PointerPressed(TerminalInput::MouseButtonState buttonState, + const unsigned int pointerUpdateKind, + const uint64_t timestamp, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const til::point pixelPosition) + { + const til::point terminalPosition = _getTerminalPosition(pixelPosition); + + const auto altEnabled = modifiers.IsAltPressed(); + const auto shiftEnabled = modifiers.IsShiftPressed(); + const auto ctrlEnabled = modifiers.IsCtrlPressed(); + + // GH#9396: we prioritize hyper-link over VT mouse events + auto hyperlink = _core->GetHyperlink(terminalPosition); + if (buttonState.isLeftButtonDown && + ctrlEnabled && !hyperlink.empty()) + { + const auto clickCount = _numberOfClicks(pixelPosition, timestamp); + // Handle hyper-link only on the first click to prevent multiple activations + if (clickCount == 1) + { + _hyperlinkHandler(hyperlink); + } + } + else if (_canSendVTMouseInput(modifiers)) + { + _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); + } + else if (buttonState.isLeftButtonDown) + { + const auto clickCount = _numberOfClicks(pixelPosition, 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 auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ((clickCount + MAX_CLICK_COUNT - 1) % MAX_CLICK_COUNT) + 1 : clickCount; + + // Capture the position of the first click when no selection is active + if (multiClickMapper == 1) + { + _singleClickTouchdownPos = pixelPosition; + _singleClickTouchdownTerminalPos = terminalPosition; + + if (!_core->HasSelection()) + { + _lastMouseClickPosNoSelection = pixelPosition; + } + } + const bool isOnOriginalPosition = _lastMouseClickPosNoSelection == pixelPosition; + + _core->LeftClickOnTerminal(terminalPosition, + multiClickMapper, + altEnabled, + shiftEnabled, + isOnOriginalPosition, + _selectionNeedsToBeCopied); + + if (_core->HasSelection()) + { + // GH#9787: if selection is active we don't want to track the touchdown position + // so that dragging the mouse will extend the selection rather than starting the new one + _singleClickTouchdownPos = std::nullopt; + } + } + else if (buttonState.isRightButtonDown) + { + // CopyOnSelect right click always pastes + if (_core->CopyOnSelect() || !_core->HasSelection()) + { + RequestPasteTextFromClipboard(); + } + else + { + CopySelectionToClipboard(shiftEnabled, nullptr); + } + } + } + + void ControlInteractivity::TouchPressed(const til::point contactPoint) + { + _touchAnchor = contactPoint; + } + + void ControlInteractivity::PointerMoved(TerminalInput::MouseButtonState buttonState, + const unsigned int pointerUpdateKind, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const bool focused, + const til::point pixelPosition) + { + const til::point terminalPosition = _getTerminalPosition(pixelPosition); + + // Short-circuit isReadOnly check to avoid warning dialog + if (focused && !_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) + { + _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); + } + else if (focused && buttonState.isLeftButtonDown) + { + if (_singleClickTouchdownPos) + { + // Figure out if the user's moved a quarter of a cell's smaller axis away from the clickdown point + auto& touchdownPoint{ *_singleClickTouchdownPos }; + float dx = ::base::saturated_cast(pixelPosition.x() - touchdownPoint.x()); + float dy = ::base::saturated_cast(pixelPosition.y() - touchdownPoint.y()); + auto distance{ std::sqrtf(std::powf(dx, 2) + + std::powf(dy, 2)) }; + + const auto fontSizeInDips{ _core->FontSizeInDips() }; + if (distance >= (std::min(fontSizeInDips.width(), fontSizeInDips.height()) / 4.f)) + { + _core->SetSelectionAnchor(terminalPosition); + // stop tracking the touchdown point + _singleClickTouchdownPos = std::nullopt; + _singleClickTouchdownTerminalPos = std::nullopt; + } + } + + SetEndSelectionPoint(pixelPosition); + } + + _core->UpdateHoveredCell(terminalPosition); + } + + void ControlInteractivity::TouchMoved(const til::point newTouchPoint, + const bool focused) + { + if (focused && + _touchAnchor) + { + const auto anchor = _touchAnchor.value(); + + // Our actualFont's size is in pixels, convert to DIPs, which the + // rest of the Points here are in. + const auto fontSizeInDips{ _core->FontSizeInDips() }; + + // Get the difference between the point we've dragged to and the start of the touch. + const float dy = ::base::saturated_cast(newTouchPoint.y() - anchor.y()); + + // Start viewport scroll after we've moved more than a half row of text + if (std::abs(dy) > (fontSizeInDips.height() / 2.0f)) + { + // 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 / fontSizeInDips.height()); + + const auto currentOffset = ::base::ClampedNumeric(_core->ScrollOffset()); + const auto newValue = numRows + currentOffset; + + // Update the Core's viewport position, and raise a + // ScrollPositionChanged event to update the scrollbar + _updateScrollbar(newValue); + + // Use this point as our new scroll anchor. + _touchAnchor = newTouchPoint; + } + } + } + + void ControlInteractivity::PointerReleased(TerminalInput::MouseButtonState buttonState, + const unsigned int pointerUpdateKind, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const til::point pixelPosition) + { + const til::point terminalPosition = _getTerminalPosition(pixelPosition); + // Short-circuit isReadOnly check to avoid warning dialog + if (!_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) + { + _core->SendMouseEvent(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState); + return; + } + + // Only a left click release when copy on select is active should perform a copy. + // Right clicks and middle clicks should not need to do anything when released. + const bool isLeftMouseRelease = pointerUpdateKind == WM_LBUTTONUP; + + if (_core->CopyOnSelect() && + isLeftMouseRelease && + _selectionNeedsToBeCopied) + { + CopySelectionToClipboard(false, nullptr); + } + + _singleClickTouchdownPos = std::nullopt; + _singleClickTouchdownTerminalPos = std::nullopt; + } + + void ControlInteractivity::TouchReleased() + { + _touchAnchor = std::nullopt; + } + + // Method Description: + // - Actually handle a scrolling event, whether from a mouse wheel or a + // touchpad scroll. Depending upon what modifier keys are pressed, + // different actions will take place. + // * Attempts to first dispatch the mouse scroll as a VT event + // * If Ctrl+Shift are pressed, then attempts to change our opacity + // * If just Ctrl is pressed, we'll attempt to "zoom" by changing our font size + // * Otherwise, just scrolls the content of the viewport + // Arguments: + // - point: the location of the mouse during this event + // - modifiers: The modifiers pressed during this event, in the form of a VirtualKeyModifiers + // - delta: the mouse wheel delta that triggered this event. + bool ControlInteractivity::MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const int32_t delta, + const til::point pixelPosition, + const TerminalInput::MouseButtonState state) + { + const til::point terminalPosition = _getTerminalPosition(pixelPosition); + + // Short-circuit isReadOnly check to avoid warning dialog + if (!_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers)) + { + // Most mouse event handlers call + // _trySendMouseEvent(point); + // here with a PointerPoint. However, as of #979, we don't have a + // PointerPoint to work with. So, we're just going to do a + // mousewheel event manually + return _core->SendMouseEvent(terminalPosition, + WM_MOUSEWHEEL, + modifiers, + ::base::saturated_cast(delta), + state); + } + + const auto ctrlPressed = modifiers.IsCtrlPressed(); + const auto shiftPressed = modifiers.IsShiftPressed(); + + if (ctrlPressed && shiftPressed) + { + _mouseTransparencyHandler(delta); + } + else if (ctrlPressed) + { + _mouseZoomHandler(delta); + } + else + { + _mouseScrollHandler(delta, terminalPosition, state.isLeftButtonDown); + } + return false; + } + + // 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 ControlInteractivity::_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; + _core->AdjustOpacity(effectiveDelta); + } + + // 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 ControlInteractivity::_mouseZoomHandler(const double mouseDelta) + { + const auto fontDelta = mouseDelta < 0 ? -1 : 1; + _core->AdjustFontSize(fontDelta); + } + + // Method Description: + // - Scroll the visible viewport in response to a mouse wheel event. + // Arguments: + // - mouseDelta: the mouse wheel delta that triggered this event. + // - point: the location of the mouse during this event + // - isLeftButtonPressed: true iff the left mouse button was pressed during this event. + void ControlInteractivity::_mouseScrollHandler(const double mouseDelta, + const til::point terminalPosition, + const bool isLeftButtonPressed) + { + const auto currentOffset = _core->ScrollOffset(); + + // negative = down, positive = up + // However, for us, the signs are flipped. + // With one of the precision mice, one click is always a multiple of 120 (WHEEL_DELTA), + // but the "smooth scrolling" mode results in non-int values + const auto rowDelta = mouseDelta / (-1.0 * WHEEL_DELTA); + + // WHEEL_PAGESCROLL is a Win32 constant that represents the "scroll one page + // at a time" setting. If we ignore it, we will scroll a truly absurd number + // of rows. + const auto rowsToScroll{ _rowsToScroll == WHEEL_PAGESCROLL ? _core->ViewHeight() : _rowsToScroll }; + double newValue = (rowsToScroll * rowDelta) + (currentOffset); + + // Update the Core's viewport position, and raise a + // ScrollPositionChanged event to update the scrollbar + _updateScrollbar(::base::saturated_cast(newValue)); + + if (isLeftButtonPressed) + { + // If user is mouse selecting and scrolls, they then point at new + // character. Make sure selection reflects that immediately. + SetEndSelectionPoint(terminalPosition); + } + } + + // Method Description: + // - Update the scroll position in such a way that should update the + // scrollbar. For example, when scrolling the buffer with the mouse or + // touch input. This will both update the Core's Terminal's buffer + // location, then also raise our own ScrollPositionChanged event. + // UserScrollViewport _won't_ raise the core's ScrollPositionChanged + // event, because it's assumed that's already being called from a context + // that knows about the change to the scrollbar. So we need to raise the + // event on our own. + // - The hosting control should make sure to listen to our own + // ScrollPositionChanged event and use that as an opportunity to update + // the location of the scrollbar. + // Arguments: + // - newValue: The new top of the viewport + // Return Value: + // - + void ControlInteractivity::_updateScrollbar(const int newValue) + { + _core->UserScrollViewport(newValue); + + // _core->ScrollOffset() is now set to newValue + _ScrollPositionChangedHandlers(*this, + winrt::make(_core->ScrollOffset(), + _core->ViewHeight(), + _core->BufferHeight())); + } + + void ControlInteractivity::_hyperlinkHandler(const std::wstring_view uri) + { + _OpenHyperlinkHandlers(*this, winrt::make(winrt::hstring{ uri })); + } + + bool ControlInteractivity::_canSendVTMouseInput(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers) + { + // If the user is holding down Shift, suppress mouse events + // TODO GH#4875: disable/customize this functionality + if (modifiers.IsShiftPressed()) + { + return false; + } + return _core->IsVtMouseModeEnabled(); + } + + // 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 ControlInteractivity::SetEndSelectionPoint(const til::point pixelPosition) + { + _core->SetEndSelectionPoint(_getTerminalPosition(pixelPosition)); + _selectionNeedsToBeCopied = true; + } + + // Method Description: + // - Gets the corresponding viewport terminal position for the point in + // pixels, by normalizing with the font size. + // Arguments: + // - pixelPosition: the (x,y) position of a given point (i.e.: mouse cursor). + // NOTE: origin (0,0) is top-left. + // Return Value: + // - the corresponding viewport terminal position for the given Point parameter + til::point ControlInteractivity::_getTerminalPosition(const til::point& pixelPosition) + { + // Get the size of the font, which is in pixels + const til::size fontSize{ _core->GetFont().GetSize() }; + // Convert the location in pixels to characters within the current viewport. + return til::point{ pixelPosition / fontSize }; + } +} diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h new file mode 100644 index 00000000000..cd099dff3a4 --- /dev/null +++ b/src/cascadia/TerminalControl/ControlInteractivity.h @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// Module Name: +// - ControlInteractivity.h +// +// Abstract: +// - This is a wrapper for the `ControlCore`. It holds the logic for things like +// double-click, right click copy/paste, selection, etc. This is intended to +// be a UI framework-independent abstraction. The methods this layer exposes +// can be called the same from both the WinUI `TermControl` and the WPF +// control. +// +// Author: +// - Mike Griese (zadjii-msft) 01-Apr-2021 + +#pragma once + +#include "ControlInteractivity.g.h" +#include "EventArgs.h" +#include "../buffer/out/search.h" +#include "cppwinrt_utils.h" + +#include "ControlCore.h" + +namespace Microsoft::Console::VirtualTerminal +{ + struct MouseButtonState; +} + +namespace ControlUnitTests +{ + class ControlCoreTests; + class ControlInteractivityTests; +}; + +namespace winrt::Microsoft::Terminal::Control::implementation +{ + struct ControlInteractivity : ControlInteractivityT + { + public: + ControlInteractivity(IControlSettings settings, + TerminalConnection::ITerminalConnection connection); + + void GainFocus(); + void UpdateSettings(); + void Initialize(); + winrt::com_ptr GetCore(); + +#pragma region Input Methods + void PointerPressed(::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState buttonState, + const unsigned int pointerUpdateKind, + const uint64_t timestamp, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const til::point pixelPosition); + void TouchPressed(const til::point contactPoint); + + void PointerMoved(::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState buttonState, + const unsigned int pointerUpdateKind, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const bool focused, + const til::point pixelPosition); + void TouchMoved(const til::point newTouchPoint, + const bool focused); + + void PointerReleased(::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState buttonState, + const unsigned int pointerUpdateKind, + const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const til::point pixelPosition); + void TouchReleased(); + + bool MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, + const int32_t delta, + const til::point pixelPosition, + const ::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state); +#pragma endregion + + bool CopySelectionToClipboard(bool singleLine, + const Windows::Foundation::IReference& formats); + void RequestPasteTextFromClipboard(); + void SetEndSelectionPoint(const til::point pixelPosition); + + private: + winrt::com_ptr _core{ nullptr }; + unsigned int _rowsToScroll; + + // If this is set, then we assume we are in the middle of panning the + // viewport via touch input. + std::optional _touchAnchor; + + using Timestamp = uint64_t; + + // imported from WinUser + // Used for PointerPoint.Timestamp Property (https://docs.microsoft.com/en-us/uwp/api/windows.ui.input.pointerpoint.timestamp#Windows_UI_Input_PointerPoint_Timestamp) + Timestamp _multiClickTimer; + unsigned int _multiClickCounter; + Timestamp _lastMouseClickTimestamp; + std::optional _lastMouseClickPos; + std::optional _singleClickTouchdownPos; + std::optional _singleClickTouchdownTerminalPos; + std::optional _lastMouseClickPosNoSelection; + // This field tracks whether the selection has changed meaningfully + // since it was last copied. It's generally used to prevent copyOnSelect + // from firing when the pointer _just happens_ to be released over the + // terminal. + bool _selectionNeedsToBeCopied; + + std::optional _lastHoveredCell{ std::nullopt }; + // Track the last hyperlink ID we hovered over + uint16_t _lastHoveredId{ 0 }; + + std::optional::interval> _lastHoveredInterval{ std::nullopt }; + + unsigned int _numberOfClicks(til::point clickPos, Timestamp clickTime); + void _updateSystemParameterSettings() noexcept; + + void _mouseTransparencyHandler(const double mouseDelta); + void _mouseZoomHandler(const double mouseDelta); + void _mouseScrollHandler(const double mouseDelta, + const til::point terminalPosition, + const bool isLeftButtonPressed); + + void _hyperlinkHandler(const std::wstring_view uri); + bool _canSendVTMouseInput(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers); + + void _sendPastedTextToConnection(std::wstring_view wstr); + void _updateScrollbar(const int newValue); + til::point _getTerminalPosition(const til::point& pixelPosition); + + TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); + TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs); + TYPED_EVENT(ScrollPositionChanged, IInspectable, Control::ScrollPositionChangedArgs); + + friend class ControlUnitTests::ControlCoreTests; + friend class ControlUnitTests::ControlInteractivityTests; + }; +} + +namespace winrt::Microsoft::Terminal::Control::factory_implementation +{ + BASIC_FACTORY(ControlInteractivity); +} diff --git a/src/cascadia/TerminalControl/ControlInteractivity.idl b/src/cascadia/TerminalControl/ControlInteractivity.idl new file mode 100644 index 00000000000..3a9afaa1409 --- /dev/null +++ b/src/cascadia/TerminalControl/ControlInteractivity.idl @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import "ICoreState.idl"; +import "IControlSettings.idl"; +import "ControlCore.idl"; +import "EventArgs.idl"; + +namespace Microsoft.Terminal.Control +{ + + [default_interface] runtimeclass ControlInteractivity + { + ControlInteractivity(IControlSettings settings, + Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); + }; +} diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index 3586f0314a9..f4b4d9bc786 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -10,3 +10,4 @@ #include "NoticeEventArgs.g.cpp" #include "ScrollPositionChangedArgs.g.cpp" #include "RendererWarningArgs.g.cpp" +#include "TransparencyChangedEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index dfc31ad441c..92b60f393d1 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -10,12 +10,12 @@ #include "NoticeEventArgs.g.h" #include "ScrollPositionChangedArgs.g.h" #include "RendererWarningArgs.g.h" +#include "TransparencyChangedEventArgs.g.h" #include "cppwinrt_utils.h" namespace winrt::Microsoft::Terminal::Control::implementation { - struct TitleChangedEventArgs : - public TitleChangedEventArgsT + struct TitleChangedEventArgs : public TitleChangedEventArgsT { public: TitleChangedEventArgs(hstring title) : @@ -24,8 +24,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(hstring, Title); }; - struct CopyToClipboardEventArgs : - public CopyToClipboardEventArgsT + struct CopyToClipboardEventArgs : public CopyToClipboardEventArgsT { public: CopyToClipboardEventArgs(hstring text) : @@ -52,24 +51,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::IReference _formats; }; - struct PasteFromClipboardEventArgs : - public PasteFromClipboardEventArgsT + struct PasteFromClipboardEventArgs : public PasteFromClipboardEventArgsT { public: - PasteFromClipboardEventArgs(std::function clipboardDataHandler) : + PasteFromClipboardEventArgs(std::function clipboardDataHandler) : m_clipboardDataHandler(clipboardDataHandler) {} void HandleClipboardData(hstring value) { - m_clipboardDataHandler(static_cast(value)); + m_clipboardDataHandler(value); }; private: - std::function m_clipboardDataHandler; + std::function m_clipboardDataHandler; }; - struct OpenHyperlinkEventArgs : - public OpenHyperlinkEventArgsT + struct OpenHyperlinkEventArgs : public OpenHyperlinkEventArgsT { public: OpenHyperlinkEventArgs(hstring uri) : @@ -81,8 +78,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation hstring _uri; }; - struct NoticeEventArgs : - public NoticeEventArgsT + struct NoticeEventArgs : public NoticeEventArgsT { public: NoticeEventArgs(const NoticeLevel level, const hstring& message) : @@ -97,8 +93,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const hstring _message; }; - struct ScrollPositionChangedArgs : - public ScrollPositionChangedArgsT + struct ScrollPositionChangedArgs : public ScrollPositionChangedArgsT { public: ScrollPositionChangedArgs(const int viewTop, @@ -115,8 +110,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(int32_t, BufferSize); }; - struct RendererWarningArgs : - public RendererWarningArgsT + struct RendererWarningArgs : public RendererWarningArgsT { public: RendererWarningArgs(const uint64_t hr) : @@ -126,4 +120,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(uint64_t, Result); }; + + struct TransparencyChangedEventArgs : public TransparencyChangedEventArgsT + { + public: + TransparencyChangedEventArgs(const double opacity) : + _Opacity(opacity) + { + } + + WINRT_PROPERTY(double, Opacity); + }; } diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 4fe7ff89115..dae093f51f8 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -62,4 +62,9 @@ namespace Microsoft.Terminal.Control { UInt64 Result { get; }; } + + runtimeclass TransparencyChangedEventArgs + { + Double Opacity { get; }; + } } diff --git a/src/cascadia/TerminalControl/ICoreState.idl b/src/cascadia/TerminalControl/ICoreState.idl new file mode 100644 index 00000000000..a21c0747c3e --- /dev/null +++ b/src/cascadia/TerminalControl/ICoreState.idl @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +namespace Microsoft.Terminal.Control +{ + // These are properties of the TerminalCore that should be queryable by the + // rest of the app. + interface ICoreState + { + String Title { get; }; + UInt64 TaskbarState { get; }; + UInt64 TaskbarProgress { get; }; + + String WorkingDirectory { get; }; + + Windows.Foundation.IReference TabColor { get; }; + + Int32 ScrollOffset { get; }; + Int32 ViewHeight { get; }; + + Boolean BracketedPasteEnabled { get; }; + + Microsoft.Terminal.TerminalConnection.ConnectionState ConnectionState { get; }; + }; +} diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 79dab5af89b..5346d0507c9 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -46,27 +46,8 @@ DEFINE_ENUM_FLAG_OPERATORS(winrt::Microsoft::Terminal::Control::CopyFormat); namespace winrt::Microsoft::Terminal::Control::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(IControlSettings settings, TerminalConnection::ITerminalConnection connection) : - _connection{ connection }, + TermControl::TermControl(IControlSettings settings, + TerminalConnection::ITerminalConnection connection) : _initializedTerminal{ false }, _settings{ settings }, _closing{ false }, @@ -75,67 +56,39 @@ namespace winrt::Microsoft::Terminal::Control::implementation _autoScrollingPointerPoint{ std::nullopt }, _autoScrollTimer{}, _lastAutoScrollUpdateTime{ std::nullopt }, - _desiredFont{ DEFAULT_FONT_FACE, 0, DEFAULT_FONT_WEIGHT, { 0, DEFAULT_FONT_SIZE }, CP_UTF8 }, - _actualFont{ DEFAULT_FONT_FACE, 0, DEFAULT_FONT_WEIGHT, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false }, - _touchAnchor{ std::nullopt }, _cursorTimer{}, _blinkTimer{}, - _lastMouseClickTimestamp{}, - _lastMouseClickPos{}, - _lastMouseClickPosNoSelection{}, - _selectionNeedsToBeCopied{ false }, _searchBox{ nullptr } { - _EnsureStaticInitialization(); InitializeComponent(); - _terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>(); - - // GH#8969: pre-seed working directory to prevent potential races - _terminal->SetWorkingDirectory(_settings.StartingDirectory()); - - auto pfnWarningBell = [this]() { - _playWarningBell->Run(); - }; - _terminal->SetWarningBellCallback(pfnWarningBell); - - auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1); - _terminal->SetTitleChangedCallback(pfnTitleChanged); - - auto pfnTabColorChanged = std::bind(&TermControl::_TerminalTabColorChanged, this, std::placeholders::_1); - _terminal->SetTabColorChangedCallback(pfnTabColorChanged); - - 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); - - auto pfnTerminalCursorPositionChanged = std::bind(&TermControl::_TerminalCursorPositionChanged, this); - _terminal->SetCursorPositionChangedCallback(pfnTerminalCursorPositionChanged); - - auto pfnCopyToClipboard = std::bind(&TermControl::_CopyToClipboard, this, std::placeholders::_1); - _terminal->SetCopyToClipboardCallback(pfnCopyToClipboard); - - _terminal->TaskbarProgressChangedCallback([&]() { TermControl::TaskbarProgressChanged(); }); - - // This event is explicitly revoked in the destructor: does not need weak_ref - auto onReceiveOutputFn = [this](const hstring str) { - _terminal->Write(str); - _updatePatternLocations->Run(); - }; - _connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn); - - _terminal->SetWriteInputCallback([this](std::wstring& wstr) { - _SendInputToConnection(wstr); - }); - - _terminal->UpdateSettings(settings); - - // Subscribe to the connection's disconnected event and call our connection closed handlers. - _connectionStateChangedRevoker = _connection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*v*/) { - _ConnectionStateChangedHandlers(*this, nullptr); - }); + _interactivity = winrt::make_self(settings, connection); + _core = _interactivity->GetCore(); + + // Use a manual revoker on the output event, so we can immediately stop + // worrying about it on destruction. + _coreOutputEventToken = _core->ReceivedOutput({ this, &TermControl::_coreReceivedOutput }); + + // These events might all be triggered by the connection, but that + // should be drained and closed before we complete destruction. So these + // are safe. + _core->ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); + _core->WarningBell({ this, &TermControl::_coreWarningBell }); + _core->CursorPositionChanged({ this, &TermControl::_CursorPositionChanged }); + + // This event is specifically triggered by the renderer thread, a BG thread. Use a weak ref here. + _core->RendererEnteredErrorState({ get_weak(), &TermControl::_RendererEnteredErrorState }); + + // These callbacks can only really be triggered by UI interactions. So + // they don't need weak refs - they can't be triggered unless we're + // alive. + _core->BackgroundColorChanged({ this, &TermControl::_BackgroundColorChangedHandler }); + _core->FontSizeChanged({ this, &TermControl::_coreFontSizeChanged }); + _core->TransparencyChanged({ this, &TermControl::_coreTransparencyChanged }); + _core->RaiseNotice({ this, &TermControl::_coreRaisedNotice }); + _core->HoveredHyperlinkChanged({ this, &TermControl::_hoveredHyperlinkChanged }); + _interactivity->OpenHyperlink({ this, &TermControl::_HyperlinkHandler }); + _interactivity->ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); // 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 @@ -152,6 +105,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation } }); + // Many of these ThrottledFunc's should be inside ControlCore. However, + // currently they depend on the Dispatcher() of the UI thread, which the + // Core eventually won't have access to. When we get to + // https://github.com/microsoft/terminal/projects/5#card-50760282 + // then we'll move the applicable ones. _tsfTryRedrawCanvas = std::make_shared>( [weakThis = get_weak()]() { if (auto control{ weakThis.get() }) @@ -166,7 +124,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation [weakThis = get_weak()]() { if (auto control{ weakThis.get() }) { - control->UpdatePatternLocations(); + control->_core->UpdatePatternLocations(); } }, UpdatePatternLocationsInterval, @@ -176,7 +134,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation [weakThis = get_weak()]() { if (auto control{ weakThis.get() }) { - control->_TerminalWarningBell(); + control->_WarningBellHandlers(*control, nullptr); } }, TerminalWarningBellInterval, @@ -196,7 +154,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation scrollBar.Maximum(update.newMaximum); scrollBar.Minimum(update.newMinimum); scrollBar.ViewportSize(update.newViewportSize); - scrollBar.LargeChange(std::max(update.newViewportSize - 1, 0.)); // scroll one "screenful" at a time when the scroll bar is clicked + // scroll one full screen worth at a time when the scroll bar is clicked + scrollBar.LargeChange(std::max(update.newViewportSize - 1, 0.)); control->_isInternalScrollBarUpdate = false; } @@ -226,15 +185,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation // If a text is selected inside terminal, use it to populate the search box. // If the search box already contains a value, it will be overridden. - if (_terminal->IsSelectionActive()) + if (_core->HasSelection()) { // Currently we populate the search box only if a single line is selected. // Empirically, multi-line selection works as well on sample scenarios, // but since code paths differ, extra work is required to ensure correctness. - const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(true); - if (bufferData.text.size() == 1) + auto bufferText = _core->SelectedText(true); + if (bufferText.size() == 1) { - const auto selectedLine{ til::at(bufferData.text, 0) }; + const auto selectedLine{ til::at(bufferText, 0) }; _searchBox->PopulateTextbox(selectedLine.data()); } } @@ -246,13 +205,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::SearchMatch(const bool goForward) { + if (_closing) + { + return; + } if (!_searchBox) { CreateSearchBoxControl(); } else { - _Search(_searchBox->TextBox().Text(), goForward, false); + _core->Search(_searchBox->TextBox().Text(), goForward, false); } } @@ -269,27 +232,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const bool goForward, const bool caseSensitive) { - if (text.size() == 0 || _closing) - { - return; - } - - const Search::Direction direction = goForward ? - Search::Direction::Forward : - Search::Direction::Backward; - - const Search::Sensitivity sensitivity = caseSensitive ? - Search::Sensitivity::CaseSensitive : - Search::Sensitivity::CaseInsensitive; - - Search search(*GetUiaData(), text.c_str(), direction, sensitivity); - auto lock = _terminal->LockForWriting(); - if (search.FindNext()) - { - _terminal->SetBlockSelection(false); - search.Select(); - _renderer->TriggerSelection(); - } + _core->Search(text, goForward, caseSensitive); } // Method Description: @@ -321,17 +264,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation // terminal. co_await winrt::resume_foreground(Dispatcher()); - // Take the lock before calling the helper functions to update the settings and appearance - auto lock = _terminal->LockForWriting(); - - _UpdateSettingsFromUIThreadUnderLock(_settings); + _UpdateSettingsFromUIThread(_settings); auto appearance = _settings.try_as(); if (!_focused && _UnfocusedAppearance) { appearance = _UnfocusedAppearance; } - _UpdateAppearanceFromUIThreadUnderLock(appearance); + _UpdateAppearanceFromUIThread(appearance); } // Method Description: @@ -343,43 +283,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Dispatch a call to the UI thread co_await winrt::resume_foreground(Dispatcher()); - // Take the lock before calling the helper function to update the appearance - auto lock = _terminal->LockForWriting(); - _UpdateAppearanceFromUIThreadUnderLock(newAppearance); - } - - // Method Description: - // - Writes the given sequence as input to the active terminal connection, - // Arguments: - // - wstr: the string of characters to write to the terminal connection. - // Return Value: - // - - void TermControl::SendInput(const winrt::hstring& wstr) - { - _SendInputToConnection(wstr); - } - - void TermControl::ToggleShaderEffects() - { - auto lock = _terminal->LockForWriting(); - // Originally, this action could be used to enable the retro effects - // even when they're set to `false` in the settings. If the user didn't - // specify a custom pixel shader, manually enable the legacy retro - // effect first. This will ensure that a toggle off->on will still work, - // even if they currently have retro effect off. - if (_settings.PixelShaderPath().empty() && !_renderEngine->GetRetroTerminalEffect()) - { - // SetRetroTerminalEffect to true will enable the effect. In this - // case, the shader effect will already be disabled (because neither - // a pixel shader nor the retro effects were originally requested). - // So we _don't_ want to toggle it again below, because that would - // toggle it back off. - _renderEngine->SetRetroTerminalEffect(true); - } - else - { - _renderEngine->ToggleShaderEffects(); - } + _UpdateAppearanceFromUIThread(newAppearance); } // Method Description: @@ -388,64 +292,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation // issue that causes one of our hstring -> wstring_view conversions to result in garbage, // but only from a coroutine context. See GH#8723. // - INVARIANT: This method must be called from the UI thread. - // - INVARIANT: This method can only be called if the caller has the writing lock on the terminal. // Arguments: // - newSettings: the new settings to set - void TermControl::_UpdateSettingsFromUIThreadUnderLock(IControlSettings newSettings) + void TermControl::_UpdateSettingsFromUIThread(IControlSettings newSettings) { if (_closing) { return; } + _core->UpdateSettings(_settings); + // Update our control settings _ApplyUISettings(_settings); - - // Update the terminal core with its new Core settings - _terminal->UpdateSettings(_settings); - - if (!_initializedTerminal) - { - // If we haven't initialized, there's no point in continuing. - // Initialization will handle the renderer settings. - return; - } - - // Update DxEngine settings under the lock - _renderEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); - _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering()); - - switch (_settings.AntialiasingMode()) - { - case TextAntialiasingMode::Cleartype: - _renderEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE); - break; - case TextAntialiasingMode::Aliased: - _renderEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_ALIASED); - break; - case TextAntialiasingMode::Grayscale: - default: - _renderEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); - break; - } - - // Refresh our font with the renderer - const auto actualFontOldSize = _actualFont.GetSize(); - _UpdateFont(); - const auto actualFontNewSize = _actualFont.GetSize(); - if (actualFontNewSize != actualFontOldSize) - { - _RefreshSizeUnderLock(); - } } // Method Description: // - Updates the appearance // - INVARIANT: This method must be called from the UI thread. - // - INVARIANT: This method can only be called if the caller has the writing lock on the terminal. // Arguments: // - newAppearance: the new appearance to set - void TermControl::_UpdateAppearanceFromUIThreadUnderLock(IControlAppearance newAppearance) + void TermControl::_UpdateAppearanceFromUIThread(IControlAppearance newAppearance) { if (_closing) { @@ -486,24 +353,30 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Update our control settings const auto bg = newAppearance.DefaultBackground(); - _BackgroundColorChanged(bg); + _changeBackgroundColor(bg); // Set TSF Foreground Media::SolidColorBrush foregroundBrush{}; foregroundBrush.Color(static_cast(newAppearance.DefaultForeground())); TSFInputControl().Foreground(foregroundBrush); - // Update the terminal core with its new Core settings - _terminal->UpdateAppearance(newAppearance); + _core->UpdateAppearance(newAppearance); + } - if (_renderEngine) - { - // Update DxEngine settings under the lock - _renderEngine->SetSelectionBackground(til::color{ newAppearance.SelectionBackground() }); - _renderEngine->SetRetroTerminalEffect(newAppearance.RetroTerminalEffect()); - _renderEngine->SetPixelShaderPath(newAppearance.PixelShaderPath()); - _renderer->TriggerRedrawAll(); - } + // Method Description: + // - Writes the given sequence as input to the active terminal connection, + // Arguments: + // - wstr: the string of characters to write to the terminal connection. + // Return Value: + // - + void TermControl::SendInput(const winrt::hstring& wstr) + { + _core->SendInput(wstr); + } + + void TermControl::ToggleShaderEffects() + { + _core->ToggleShaderEffects(); } // Method Description: @@ -522,23 +395,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _InitializeBackgroundBrush(); + const auto bg = newSettings.DefaultBackground(); + _changeBackgroundColor(bg); + // Apply padding as swapChainPanel's margin auto newMargin = _ParseThicknessFromPadding(newSettings.Padding()); SwapChainPanel().Margin(newMargin); - // Initialize our font information. - const auto fontFace = newSettings.FontFace(); - const short fontHeight = gsl::narrow_cast(newSettings.FontSize()); - const auto fontWeight = newSettings.FontWeight(); - // 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, fontWeight.Weight, { 0, fontHeight }, CP_UTF8, false }; - _desiredFont = { _actualFont }; - TSFInputControl().Margin(newMargin); // Apply settings for scrollbar @@ -557,7 +420,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation ScrollBar().Visibility(Visibility::Visible); } - _UpdateSystemParameterSettings(); + _interactivity->UpdateSettings(); } // Method Description: @@ -606,10 +469,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // GH#5098: Inform the engine of the new opacity of the default text background. - if (_renderEngine) - { - _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); - } + _core->SetBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); } else { @@ -617,10 +477,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation RootGrid().Background(solidColor); // GH#5098: Inform the engine of the new opacity of the default text background. - if (_renderEngine) - { - _renderEngine->SetDefaultTextBackgroundOpacity(1.0f); - } + _core->SetBackgroundOpacity(1.0f); } } @@ -630,24 +487,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - color: The background color to use as a uint32 (aka DWORD COLORREF) // Return Value: // - - winrt::fire_and_forget TermControl::_BackgroundColorChanged(const til::color color) + void TermControl::_BackgroundColorChangedHandler(const IInspectable& /*sender*/, + const IInspectable& /*args*/) { - til::color newBgColor{ color }; + til::color newBgColor{ _core->BackgroundColor() }; + _changeBackgroundColor(newBgColor); + } + winrt::fire_and_forget TermControl::_changeBackgroundColor(const til::color bg) + { auto weakThis{ get_weak() }; - co_await winrt::resume_foreground(Dispatcher()); if (auto control{ weakThis.get() }) { if (auto acrylic = RootGrid().Background().try_as()) { - acrylic.FallbackColor(newBgColor); - acrylic.TintColor(newBgColor); + acrylic.FallbackColor(bg); + acrylic.TintColor(bg); } else if (auto solidColor = RootGrid().Background().try_as()) { - solidColor.Color(newBgColor); + solidColor.Color(bg); } } } @@ -670,10 +531,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation { // create a custom automation peer with this code pattern: // (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers) - auto autoPeer = winrt::make_self(this); + auto autoPeer = winrt::make_self(this); _uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(autoPeer.get()); - _renderer->AddRenderEngine(_uiaEngine.get()); + _core->AttachUiaEngine(_uiaEngine.get()); return *autoPeer; } return nullptr; @@ -686,12 +547,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation ::Microsoft::Console::Types::IUiaData* TermControl::GetUiaData() const { - return _terminal.get(); + return _core->GetUiaData(); } - const FontInfo TermControl::GetActualFont() const + // This is needed for TermControlAutomationPeer. We probably could find a + // clever way around asking the core for this. + til::point TermControl::GetFontSize() const { - return _actualFont; + return _core->GetFont().GetSize(); } const Windows::UI::Xaml::Thickness TermControl::GetPadding() @@ -701,22 +564,22 @@ namespace winrt::Microsoft::Terminal::Control::implementation TerminalConnection::ConnectionState TermControl::ConnectionState() const { - return _connection.State(); + return _core->ConnectionState(); } - winrt::fire_and_forget TermControl::RenderEngineSwapChainChanged() + winrt::fire_and_forget TermControl::RenderEngineSwapChainChanged(IInspectable /*sender*/, IInspectable /*args*/) { // This event is only registered during terminal initialization, // so we don't need to check _initializedTerminal. // We also don't lock for things that come back from the renderer. - auto chain = _renderEngine->GetSwapChain(); auto weakThis{ get_weak() }; co_await winrt::resume_foreground(Dispatcher()); if (auto control{ weakThis.get() }) { - _AttachDxgiSwapChainToXaml(chain.Get()); + const auto chain = control->_core->GetSwapChain(); + _AttachDxgiSwapChainToXaml(chain); } } @@ -730,8 +593,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - hr: an HRESULT describing the warning // Return Value: // - - winrt::fire_and_forget TermControl::_RendererWarning(const HRESULT hr) + winrt::fire_and_forget TermControl::_RendererWarning(IInspectable /*sender*/, + Control::RendererWarningArgs args) { + const auto hr = static_cast(args.Result()); + auto weakThis{ get_weak() }; co_await winrt::resume_foreground(Dispatcher()); @@ -767,177 +633,109 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool TermControl::_InitializeTerminal() { - { // scope for terminalLock - auto terminalLock = _terminal->LockForWriting(); - - if (_initializedTerminal) - { - return false; - } - - const auto actualWidth = SwapChainPanel().ActualWidth(); - const auto actualHeight = SwapChainPanel().ActualHeight(); - - const auto windowWidth = actualWidth * SwapChainPanel().CompositionScaleX(); // Width() and Height() are NaN? - const auto windowHeight = actualHeight * SwapChainPanel().CompositionScaleY(); - - if (windowWidth == 0 || windowHeight == 0) - { - return false; - } - - // 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; - - _renderer->SetRendererEnteredErrorStateCallback([weakThis = get_weak()]() { - if (auto strongThis{ weakThis.get() }) - { - strongThis->_RendererEnteredErrorState(); - } - }); - - 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(true); - - 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); - LOG_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); - - 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); - - // IMPORTANT! Set this callback up sooner than later. If we do it - // after Enable, then it'll be possible to paint the frame once - // _before_ the warning handler is set up, and then warnings from - // the first paint will be ignored! - dxEngine->SetWarningCallback(std::bind(&TermControl::_RendererWarning, this, std::placeholders::_1)); - - dxEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); - dxEngine->SetSoftwareRendering(_settings.SoftwareRendering()); - - // Update DxEngine's AntialiasingMode - switch (_settings.AntialiasingMode()) - { - case TextAntialiasingMode::Cleartype: - dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE); - break; - case TextAntialiasingMode::Aliased: - dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_ALIASED); - break; - case TextAntialiasingMode::Grayscale: - default: - dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE); - break; - } - - // GH#5098: Inform the engine of the opacity of the default text background. - if (_settings.UseAcrylic()) - { - dxEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); - } - - THROW_IF_FAILED(dxEngine->Enable()); - _renderEngine = std::move(dxEngine); + if (_initializedTerminal) + { + return false; + } - _AttachDxgiSwapChainToXaml(_renderEngine->GetSwapChain().Get()); + const auto panelWidth = SwapChainPanel().ActualWidth(); + const auto panelHeight = SwapChainPanel().ActualHeight(); + const auto panelScaleX = SwapChainPanel().CompositionScaleX(); + const auto panelScaleY = SwapChainPanel().CompositionScaleY(); - // Tell the DX Engine to notify us when the swap chain changes. - // We do this after we initially set the swapchain so as to avoid unnecessary callbacks (and locking problems) - _renderEngine->SetCallback(std::bind(&TermControl::RenderEngineSwapChainChanged, this)); + const auto windowWidth = panelWidth * panelScaleX; + const auto windowHeight = panelHeight * panelScaleY; - auto bottom = _terminal->GetViewport().BottomExclusive(); - auto bufferHeight = bottom; + if (windowWidth == 0 || windowHeight == 0) + { + return false; + } - ScrollBar().Maximum(bufferHeight - bufferHeight); - ScrollBar().Minimum(0); - ScrollBar().Value(0); - ScrollBar().ViewportSize(bufferHeight); - ScrollBar().LargeChange(std::max(bufferHeight - 1, 0)); // scroll one "screenful" at a time when the scroll bar is clicked + // IMPORTANT! Set this callback up sooner rather than later. If we do it + // after Enable, then it'll be possible to paint the frame once + // _before_ the warning handler is set up, and then warnings from + // the first paint will be ignored! + _core->RendererWarning({ get_weak(), &TermControl::_RendererWarning }); - localPointerToThread->EnablePainting(); + const auto coreInitialized = _core->Initialize(panelWidth, + panelHeight, + panelScaleX); + if (!coreInitialized) + { + return false; + } + _interactivity->Initialize(); - // Set up blinking cursor - int blinkTime = GetCaretBlinkTime(); - if (blinkTime != INFINITE) - { - // Create a timer - DispatcherTimer cursorTimer; - cursorTimer.Interval(std::chrono::milliseconds(blinkTime)); - cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick }); - cursorTimer.Start(); - _cursorTimer.emplace(std::move(cursorTimer)); - } - else - { - // The user has disabled cursor blinking - _cursorTimer = std::nullopt; - } + _AttachDxgiSwapChainToXaml(_core->GetSwapChain()); - // Set up blinking attributes - BOOL animationsEnabled = TRUE; - SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0); - if (animationsEnabled && blinkTime != INFINITE) - { - // Create a timer - DispatcherTimer blinkTimer; - blinkTimer.Interval(std::chrono::milliseconds(blinkTime)); - blinkTimer.Tick({ get_weak(), &TermControl::_BlinkTimerTick }); - blinkTimer.Start(); - _blinkTimer.emplace(std::move(blinkTimer)); - } - else - { - // The user has disabled blinking - _blinkTimer = std::nullopt; - } + // Tell the DX Engine to notify us when the swap chain changes. We do + // this after we initially set the swapchain so as to avoid unnecessary + // callbacks (and locking problems) + _core->SwapChainChanged({ get_weak(), &TermControl::RenderEngineSwapChainChanged }); - // import value from WinUser (convert from milli-seconds to micro-seconds) - _multiClickTimer = GetDoubleClickTime() * 1000; + // !! LOAD BEARING !! + // Make sure you enable painting _AFTER_ calling _AttachDxgiSwapChainToXaml + // + // If you EnablePainting first, then you almost certainly won't have any + // problems when running in Debug. However, in Release, you'll run into + // issues where the Renderer starts trying to paint before we've + // actually attached the swapchain to anything, and the DxEngine is not + // prepared to handle that. + _core->EnablePainting(); + + auto bufferHeight = _core->BufferHeight(); + + ScrollBar().Maximum(bufferHeight - bufferHeight); + ScrollBar().Minimum(0); + ScrollBar().Value(0); + ScrollBar().ViewportSize(bufferHeight); + ScrollBar().LargeChange(std::max(bufferHeight - 1, 0)); // scroll one "screenful" at a time when the scroll bar is clicked + + // Set up blinking cursor + int blinkTime = GetCaretBlinkTime(); + if (blinkTime != INFINITE) + { + // Create a timer + DispatcherTimer cursorTimer; + cursorTimer.Interval(std::chrono::milliseconds(blinkTime)); + cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick }); + cursorTimer.Start(); + _cursorTimer.emplace(std::move(cursorTimer)); + } + else + { + // The user has disabled cursor blinking + _cursorTimer = std::nullopt; + } - // Focus the control here. If we do it during control initialization, then - // 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); + // Set up blinking attributes + BOOL animationsEnabled = TRUE; + SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0); + if (animationsEnabled && blinkTime != INFINITE) + { + // Create a timer + DispatcherTimer blinkTimer; + blinkTimer.Interval(std::chrono::milliseconds(blinkTime)); + blinkTimer.Tick({ get_weak(), &TermControl::_BlinkTimerTick }); + blinkTimer.Start(); + _blinkTimer.emplace(std::move(blinkTimer)); + } + else + { + // The user has disabled blinking + _blinkTimer = std::nullopt; + } - // Now that the renderer is set up, update the appearance for initialization - _UpdateAppearanceFromUIThreadUnderLock(_settings); + // Now that the renderer is set up, update the appearance for initialization + _UpdateAppearanceFromUIThread(_settings); - _initializedTerminal = true; - } // scope for TerminalLock + // Focus the control here. If we do it during control initialization, then + // 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); - // Start the connection outside of lock, because it could - // start writing output immediately. - _connection.Start(); + _initializedTerminal = true; // Likewise, run the event handlers outside of lock (they could // be reentrant) @@ -962,7 +760,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { modifiers |= ControlKeyStates::EnhancedKey; } - const bool handled = _terminal->SendCharEvent(ch, scanCode, modifiers); + const bool handled = _core->SendCharEvent(ch, scanCode, modifiers); e.Handled(handled); } @@ -974,7 +772,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool TermControl::OnDirectKeyEvent(const uint32_t vkey, const uint8_t scanCode, const bool down) { // Short-circuit isReadOnly check to avoid warning dialog - if (_isReadOnly) + if (_core->IsInReadOnlyMode()) { return false; } @@ -1060,7 +858,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); // Short-circuit isReadOnly check to avoid warning dialog - if (_isReadOnly) + if (_core->IsInReadOnlyMode()) { e.Handled(!keyDown || _TryHandleKeyBinding(vkey, scanCode, modifiers)); return; @@ -1180,39 +978,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const ControlKeyStates modifiers, const bool keyDown) { - // When there is a selection active, escape should clear it and NOT flow through - // to the terminal. With any other keypress, it should clear the selection AND - // flow through to the terminal. - // GH#6423 - don't dismiss selection if the key that was pressed was a - // modifier key. We'll wait for a real keystroke to dismiss the - // GH #7395 - don't dismiss selection when taking PrintScreen - // selection. - // GH#8522, GH#3758 - Only dismiss the selection on key _down_. If we - // dismiss on key up, then there's chance that we'll immediately dismiss - // a selection created by an action bound to a keydown. - if (_terminal->IsSelectionActive() && - !KeyEvent::IsModifierKey(vkey) && - vkey != VK_SNAPSHOT && - keyDown) - { - const CoreWindow window = CoreWindow::GetForCurrentThread(); - const auto leftWinKeyState = window.GetKeyState(VirtualKey::LeftWindows); - const auto rightWinKeyState = window.GetKeyState(VirtualKey::RightWindows); - const auto isLeftWinKeyDown = WI_IsFlagSet(leftWinKeyState, CoreVirtualKeyStates::Down); - const auto isRightWinKeyDown = WI_IsFlagSet(rightWinKeyState, CoreVirtualKeyStates::Down); - - // GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination. - if (!isLeftWinKeyDown && !isRightWinKeyDown) - { - _terminal->ClearSelection(); - _renderer->TriggerSelection(); - } - - if (vkey == VK_ESCAPE) - { - return true; - } - } + const CoreWindow window = CoreWindow::GetForCurrentThread(); if (vkey == VK_ESCAPE || vkey == VK_RETURN) @@ -1223,13 +989,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation // 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 CharacterReceived event. - const auto handled = vkey ? _terminal->SendKeyEvent(vkey, scanCode, modifiers, keyDown) : true; + const auto handled = vkey ? + _core->TrySendKeyEvent(vkey, + scanCode, + modifiers, + keyDown) : + true; if (_cursorTimer.has_value()) { // Manually show the cursor when a key is pressed. Restarting // the timer prevents flickering. - _terminal->SetCursorOn(true); + _core->CursorOn(true); _cursorTimer.value().Start(); } @@ -1247,77 +1018,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation e.Handled(true); } - // Method Description: - // - Send this particular mouse event to the terminal. - // See Terminal::SendMouseEvent for more information. - // Arguments: - // - point: the PointerPoint object representing a mouse event from our XAML input handler - bool TermControl::_TrySendMouseEvent(Windows::UI::Input::PointerPoint const& point) - { - const auto props = point.Properties(); - - // Get the terminal position relative to the viewport - const auto terminalPosition = _GetTerminalPosition(point.Position()); - - // Which mouse button changed state (and how) - unsigned int uiButton{}; - switch (props.PointerUpdateKind()) - { - case PointerUpdateKind::LeftButtonPressed: - uiButton = WM_LBUTTONDOWN; - break; - case PointerUpdateKind::LeftButtonReleased: - uiButton = WM_LBUTTONUP; - break; - case PointerUpdateKind::MiddleButtonPressed: - uiButton = WM_MBUTTONDOWN; - break; - case PointerUpdateKind::MiddleButtonReleased: - uiButton = WM_MBUTTONUP; - break; - case PointerUpdateKind::RightButtonPressed: - uiButton = WM_RBUTTONDOWN; - break; - case PointerUpdateKind::RightButtonReleased: - uiButton = WM_RBUTTONUP; - break; - default: - uiButton = WM_MOUSEMOVE; - } - - // Mouse wheel data - const short sWheelDelta = ::base::saturated_cast(props.MouseWheelDelta()); - if (sWheelDelta != 0 && !props.IsHorizontalMouseWheel()) - { - // if we have a mouse wheel delta and it wasn't a horizontal wheel motion - uiButton = WM_MOUSEWHEEL; - } - - const auto modifiers = _GetPressedModifierKeys(); - const TerminalInput::MouseButtonState state{ props.IsLeftButtonPressed(), props.IsMiddleButtonPressed(), props.IsRightButtonPressed() }; - return _terminal->SendMouseEvent(terminalPosition, uiButton, modifiers, sWheelDelta, state); - } - - // Method Description: - // - Checks if we can send vt mouse input. - // Arguments: - // - point: the PointerPoint object representing a mouse event from our XAML input handler - bool TermControl::_CanSendVTMouseInput() - { - if (!_terminal) - { - return false; - } - // If the user is holding down Shift, suppress mouse events - // TODO GH#4875: disable/customize this functionality - const auto modifiers = _GetPressedModifierKeys(); - if (modifiers.IsShiftPressed()) - { - return false; - } - return _terminal->IsTrackingMouseInput(); - } - // Method Description: // - handle a mouse click event. Begin selection process. // Arguments: @@ -1337,6 +1037,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto ptr = args.Pointer(); const auto point = args.GetCurrentPoint(*this); + const auto type = ptr.PointerDeviceType(); // We also TryShow in GotFocusHandler, but this call is specifically // for the case where the Terminal is in focus but the user closed the @@ -1349,125 +1050,20 @@ namespace winrt::Microsoft::Terminal::Control::implementation Focus(FocusState::Pointer); } - if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) + if (type == Windows::Devices::Input::PointerDeviceType::Touch) { - 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)); - const auto ctrlEnabled = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Control)); - - auto lock = _terminal->LockForWriting(); - const auto cursorPosition = point.Position(); - const auto terminalPosition = _GetTerminalPosition(cursorPosition); - - // GH#9396: we prioritize hyper-link over VT mouse events - if (point.Properties().IsLeftButtonPressed() && ctrlEnabled && !_terminal->GetHyperlinkAtPosition(terminalPosition).empty()) - { - // Handle hyper-link only on the first click to prevent multiple activations - const auto clickCount = _NumberOfClicks(cursorPosition, point.Timestamp()); - if (clickCount == 1) - { - _HyperlinkHandler(_terminal->GetHyperlinkAtPosition(terminalPosition)); - } - } - else if (_CanSendVTMouseInput()) - { - _TrySendMouseEvent(point); - } - else if (point.Properties().IsLeftButtonPressed()) - { - // Update the selection appropriately - // handle ALT key - _terminal->SetBlockSelection(altEnabled); - - const 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; - - ::Terminal::SelectionExpansionMode mode = ::Terminal::SelectionExpansionMode::Cell; - if (multiClickMapper == 1) - { - mode = ::Terminal::SelectionExpansionMode::Cell; - } - else if (multiClickMapper == 2) - { - mode = ::Terminal::SelectionExpansionMode::Word; - } - else if (multiClickMapper == 3) - { - mode = ::Terminal::SelectionExpansionMode::Line; - } - - // Capture the position of the first click - if (mode == ::Terminal::SelectionExpansionMode::Cell) - { - _singleClickTouchdownPos = cursorPosition; - if (!_terminal->IsSelectionActive()) - { - _lastMouseClickPosNoSelection = cursorPosition; - } - } - - // We reset the active selection if one of the conditions apply: - // - shift is not held - // - GH#9384: the position is the same as of the first click starting the selection - // (we need to reset selection on double-click or triple-click, so it captures the word or the line, - // rather than extending the selection) - if (_terminal->IsSelectionActive() && (!shiftEnabled || _lastMouseClickPosNoSelection == cursorPosition)) - { - // Reset the selection - _terminal->ClearSelection(); - _selectionNeedsToBeCopied = false; // there's no selection, so there's nothing to update - } - - if (shiftEnabled && _terminal->IsSelectionActive()) - { - // If shift is pressed and there is a selection we extend it using the selection mode - // (expand the "end"selection point) - _terminal->SetSelectionEnd(terminalPosition, mode); - _selectionNeedsToBeCopied = true; - } - else if (mode != ::Terminal::SelectionExpansionMode::Cell || shiftEnabled) - { - // If we are handling a double / triple-click or shift+single click - // we establish selection using the selected mode - // (expand both "start" and "end" selection points) - _terminal->MultiClickSelection(terminalPosition, mode); - _selectionNeedsToBeCopied = true; - } - - if (_terminal->IsSelectionActive()) - { - // GH#9787: if selection is active we don't want to track the touchdown position - // so that dragging the mouse will extend the selection rather than starting the new one - _singleClickTouchdownPos = std::nullopt; - } - - _renderer->TriggerSelection(); - } - else if (point.Properties().IsRightButtonPressed()) - { - if (_settings.CopyOnSelect() || !_terminal->IsSelectionActive()) - { - // CopyOnSelect right click always pastes - PasteTextFromClipboard(); - } - else - { - CopySelectionToClipboard(shiftEnabled, nullptr); - } - } + const auto contactRect = point.Properties().ContactRect(); + auto anchor = til::point{ til::math::rounding, contactRect.X, contactRect.Y }; + _interactivity->TouchPressed(anchor); } - else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) + else { - const auto contactRect = point.Properties().ContactRect(); - // Set our touch rect, to start a pan. - _touchAnchor = winrt::Windows::Foundation::Point{ contactRect.X, contactRect.Y }; + const auto cursorPosition = point.Position(); + _interactivity->PointerPressed(TermControl::GetPressedMouseButtons(point), + TermControl::GetPointerUpdateKind(point), + point.Timestamp(), + ControlKeyStates{ args.KeyModifiers() }, + _toTerminalOrigin(cursorPosition)); } args.Handled(true); @@ -1491,42 +1087,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto ptr = args.Pointer(); const auto point = args.GetCurrentPoint(*this); const auto cursorPosition = point.Position(); - const auto terminalPosition = _GetTerminalPosition(cursorPosition); + const auto pixelPosition = _toTerminalOrigin(cursorPosition); + const auto type = ptr.PointerDeviceType(); if (!_focused && _settings.FocusFollowMouse()) { _FocusFollowMouseRequestedHandlers(*this, nullptr); } - if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) + if (type == Windows::Devices::Input::PointerDeviceType::Mouse || + type == Windows::Devices::Input::PointerDeviceType::Pen) { - // Short-circuit isReadOnly check to avoid warning dialog - if (_focused && !_isReadOnly && _CanSendVTMouseInput()) - { - _TrySendMouseEvent(point); - } - else if (_focused && point.Properties().IsLeftButtonPressed()) - { - auto lock = _terminal->LockForWriting(); - - if (_singleClickTouchdownPos) - { - // Figure out if the user's moved a quarter of a cell's smaller axis away from the clickdown point - auto& touchdownPoint{ *_singleClickTouchdownPos }; - auto distance{ std::sqrtf(std::powf(cursorPosition.X - touchdownPoint.X, 2) + std::powf(cursorPosition.Y - touchdownPoint.Y, 2)) }; - const til::size fontSize{ _actualFont.GetSize() }; - - const auto fontSizeInDips = fontSize.scale(til::math::rounding, 1.0f / _renderEngine->GetScaling()); - if (distance >= (std::min(fontSizeInDips.width(), fontSizeInDips.height()) / 4.f)) - { - _terminal->SetSelectionAnchor(_GetTerminalPosition(touchdownPoint)); - // stop tracking the touchdown point - _singleClickTouchdownPos = std::nullopt; - } - } - - _SetEndSelectionPointAtCursor(cursorPosition); + _interactivity->PointerMoved(TermControl::GetPressedMouseButtons(point), + TermControl::GetPointerUpdateKind(point), + ControlKeyStates(args.KeyModifiers()), + _focused, + pixelPosition); + if (_focused && point.Properties().IsLeftButtonPressed()) + { const double cursorBelowBottomDist = cursorPosition.Y - SwapChainPanel().Margin().Top - SwapChainPanel().ActualHeight(); const double cursorAboveTopDist = -1 * cursorPosition.Y + SwapChainPanel().Margin().Top; @@ -1550,41 +1129,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation _TryStopAutoScroll(ptr.PointerId()); } } - - _UpdateHoveredCell(terminalPosition); } - else if (_focused && ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch && _touchAnchor) + else if (type == Windows::Devices::Input::PointerDeviceType::Touch) { const auto contactRect = point.Properties().ContactRect(); - winrt::Windows::Foundation::Point newTouchPoint{ contactRect.X, contactRect.Y }; - const auto anchor = _touchAnchor.value(); - - // Our _actualFont's size is in pixels, convert to DIPs, which the - // rest of the Points here are in. - const til::size fontSize{ _actualFont.GetSize() }; - const auto fontSizeInDips = fontSize.scale(til::math::rounding, 1.0f / _renderEngine->GetScaling()); + til::point newTouchPoint{ til::math::rounding, contactRect.X, contactRect.Y }; - // Get the difference between the point we've dragged to and the start of the touch. - const float dy = newTouchPoint.Y - anchor.Y; - - // Start viewport scroll after we've moved more than a half row of text - if (std::abs(dy) > (fontSizeInDips.height() / 2.0f)) - { - // 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 / fontSizeInDips.height()); - - const auto currentOffset = ::base::ClampedNumeric(ScrollBar().Value()); - const auto newValue = numRows + currentOffset; - - ScrollBar().Value(newValue); - - // Use this point as our new scroll anchor. - _touchAnchor = newTouchPoint; - } + _interactivity->TouchMoved(newTouchPoint, _focused); } + args.Handled(true); } @@ -1604,33 +1157,25 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto ptr = args.Pointer(); const auto point = args.GetCurrentPoint(*this); + const auto cursorPosition = point.Position(); + const auto pixelPosition = _toTerminalOrigin(cursorPosition); + const auto type = ptr.PointerDeviceType(); _ReleasePointerCapture(sender, args); - if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) + if (type == Windows::Devices::Input::PointerDeviceType::Mouse || + type == Windows::Devices::Input::PointerDeviceType::Pen) { - // Short-circuit isReadOnly check to avoid warning dialog - if (!_isReadOnly && _CanSendVTMouseInput()) - { - _TrySendMouseEvent(point); - args.Handled(true); - return; - } - - // Only a left click release when copy on select is active should perform a copy. - // Right clicks and middle clicks should not need to do anything when released. - if (_settings.CopyOnSelect() && point.Properties().PointerUpdateKind() == Windows::UI::Input::PointerUpdateKind::LeftButtonReleased && _selectionNeedsToBeCopied) - { - CopySelectionToClipboard(false, nullptr); - } + _interactivity->PointerReleased(TermControl::GetPressedMouseButtons(point), + TermControl::GetPointerUpdateKind(point), + ControlKeyStates(args.KeyModifiers()), + pixelPosition); } - else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) + else if (type == Windows::Devices::Input::PointerDeviceType::Touch) { - _touchAnchor = std::nullopt; + _interactivity->TouchReleased(); } - _singleClickTouchdownPos = std::nullopt; - _TryStopAutoScroll(ptr.PointerId()); args.Handled(true); @@ -1655,69 +1200,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation _RestorePointerCursorHandlers(*this, nullptr); const auto point = args.GetCurrentPoint(*this); - const auto props = point.Properties(); - const TerminalInput::MouseButtonState state{ props.IsLeftButtonPressed(), props.IsMiddleButtonPressed(), props.IsRightButtonPressed() }; - auto result = _DoMouseWheel(point.Position(), - ControlKeyStates{ args.KeyModifiers() }, - point.Properties().MouseWheelDelta(), - state); + + auto result = _interactivity->MouseWheel(ControlKeyStates{ args.KeyModifiers() }, + point.Properties().MouseWheelDelta(), + _toTerminalOrigin(point.Position()), + TermControl::GetPressedMouseButtons(point)); if (result) { args.Handled(true); } } - // Method Description: - // - Actually handle a scrolling event, whether from a mouse wheel or a - - // touchpad scroll. Depending upon what modifier keys are pressed, - // different actions will take place. - // * Attempts to first dispatch the mouse scroll as a VT event - // * If Ctrl+Shift are pressed, then attempts to change our opacity - // * If just Ctrl is pressed, we'll attempt to "zoom" by changing our font size - // * Otherwise, just scrolls the content of the viewport - // Arguments: - // - point: the location of the mouse during this event - // - modifiers: The modifiers pressed during this event, in the form of a VirtualKeyModifiers - // - delta: the mouse wheel delta that triggered this event. - bool TermControl::_DoMouseWheel(const Windows::Foundation::Point point, - const ControlKeyStates modifiers, - const int32_t delta, - const TerminalInput::MouseButtonState state) - { - // Short-circuit isReadOnly check to avoid warning dialog - if (!_isReadOnly && _CanSendVTMouseInput()) - { - // Most mouse event handlers call - // _TrySendMouseEvent(point); - // here with a PointerPoint. However, as of #979, we don't have a - // PointerPoint to work with. So, we're just going to do a - // mousewheel event manually - return _terminal->SendMouseEvent(_GetTerminalPosition(point), - WM_MOUSEWHEEL, - _GetPressedModifierKeys(), - ::base::saturated_cast(delta), - state); - } - - const auto ctrlPressed = modifiers.IsCtrlPressed(); - const auto shiftPressed = modifiers.IsShiftPressed(); - - if (ctrlPressed && shiftPressed) - { - _MouseTransparencyHandler(delta); - } - else if (ctrlPressed) - { - _MouseZoomHandler(delta); - } - else - { - _MouseScrollHandler(delta, point, state.isLeftButtonDown); - } - return false; - } - // Method Description: // - This is part of the solution to GH#979 // - Manually handle a scrolling event. This is used to help support @@ -1735,74 +1228,49 @@ namespace winrt::Microsoft::Terminal::Control::implementation const bool rightButtonDown) { const auto modifiers = _GetPressedModifierKeys(); - TerminalInput::MouseButtonState state{ leftButtonDown, midButtonDown, rightButtonDown }; - return _DoMouseWheel(location, modifiers, delta, state); - } - - // Method Description: - // - Tell TerminalCore to update its knowledge about the locations of visible regex patterns - // - We should call this (through the throttled function) when something causes the visible - // region to change, such as when new text enters the buffer or the viewport is scrolled - void TermControl::UpdatePatternLocations() - { - _terminal->UpdatePatterns(); + TerminalInput::MouseButtonState state{ leftButtonDown, + midButtonDown, + rightButtonDown }; + return _interactivity->MouseWheel(modifiers, delta, _toTerminalOrigin(location), state); } // Method Description: - // - Adjust the opacity of the acrylic background in response to a mouse - // scrolling event. + // - Called in response to the core's TransparencyChanged event. We'll use + // this to update our background brush. + // - The Core should have already updated the TintOpacity and UseAcrylic + // properties in the _settings. // Arguments: - // - mouseDelta: the mouse wheel delta that triggered this event. - void TermControl::_MouseTransparencyHandler(const double mouseDelta) + // - + // Return Value: + // - + winrt::fire_and_forget TermControl::_coreTransparencyChanged(IInspectable /*sender*/, + Control::TransparencyChangedEventArgs /*args*/) { - // 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 = RootGrid().Background().as(); - _settings.TintOpacity(acrylicBrush.TintOpacity() + effectiveDelta); - acrylicBrush.TintOpacity(_settings.TintOpacity()); - - if (acrylicBrush.TintOpacity() == 1.0) - { - _settings.UseAcrylic(false); - _InitializeBackgroundBrush(); - const auto bg = _settings.DefaultBackground(); - _BackgroundColorChanged(bg); - } - else - { - // GH#5098: Inform the engine of the new opacity of the default text background. - if (_renderEngine) - { - _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.TintOpacity())); - } - } - } - CATCH_LOG(); - } - else if (mouseDelta < 0) + co_await resume_foreground(Dispatcher()); + try { - _settings.UseAcrylic(true); - - //Setting initial opacity set to 1 to ensure smooth transition to acrylic during mouse scroll - _settings.TintOpacity(1.0); _InitializeBackgroundBrush(); + const auto bg = _settings.DefaultBackground(); + _changeBackgroundColor(bg); } + 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) + + void TermControl::_coreReceivedOutput(const IInspectable& /*sender*/, + const IInspectable& /*args*/) { - const auto fontDelta = mouseDelta < 0 ? -1 : 1; - AdjustFontSize(fontDelta); + // Queue up a throttled UpdatePatternLocations call. In the future, we + // should have the _updatePatternLocations ThrottledFunc internal to + // ControlCore, and run on that object's dispatcher queue. + // + // We're not doing that quite yet, because the Core will eventually + // be out-of-proc from the UI thread, and won't be able to just use + // the UI thread as the dispatcher queue thread. + // + // THIS IS CALLED ON EVERY STRING OF TEXT OUTPUT TO THE TERMINAL. Think + // twice before adding anything here. + + _updatePatternLocations->Run(); } // Method Description: @@ -1811,7 +1279,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - none void TermControl::ResetFontSize() { - _SetFontSize(_settings.FontSize()); + _core->ResetFontSize(); } // Method Description: @@ -1820,47 +1288,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - 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. - // - point: the location of the mouse during this event - // - isLeftButtonPressed: true iff the left mouse button was pressed during this event. - void TermControl::_MouseScrollHandler(const double mouseDelta, - const Windows::Foundation::Point point, - const bool isLeftButtonPressed) - { - const auto currentOffset = ScrollBar().Value(); - - // negative = down, positive = up - // However, for us, the signs are flipped. - // With one of the precision mice, one click is always a multiple of 120 (WHEEL_DELTA), - // but the "smooth scrolling" mode results in non-int values - const auto rowDelta = mouseDelta / (-1.0 * WHEEL_DELTA); - - // WHEEL_PAGESCROLL is a Win32 constant that represents the "scroll one page - // at a time" setting. If we ignore it, we will scroll a truly absurd number - // of rows. - const auto rowsToScroll{ _rowsToScroll == WHEEL_PAGESCROLL ? GetViewHeight() : _rowsToScroll }; - double newValue = (rowsToScroll * rowDelta) + (currentOffset); - - // The scroll bar's ValueChanged handler will actually move the viewport - // for us. - ScrollBar().Value(newValue); - - if (_terminal->IsSelectionActive() && isLeftButtonPressed) - { - // Have to take the lock or we could change the endpoints out from under the renderer actively rendering. - auto lock = _terminal->LockForWriting(); - - // If user is mouse selecting and scrolls, they then point at new character. - // Make sure selection reflects that immediately. - _SetEndSelectionPointAtCursor(point); - } + _core->AdjustFontSize(fontSizeDelta); } void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/, @@ -1874,21 +1302,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - // Clear the regex pattern tree so the renderer does not try to render them while scrolling - { - // We're taking the lock here instead of in ClearPatternTree because ClearPatternTree is - // sometimes called from an already-locked context. Here, we are sure we are not - // already under lock (since it is not an internal scroll bar update) - // TODO GH#9617: refine locking around pattern tree - auto lock = _terminal->LockForWriting(); - _terminal->ClearPatternTree(); - } - const auto newValue = static_cast(args.NewValue()); - - // This is a scroll event that wasn't initiated by the terminal - // itself - it was initiated by the mouse wheel, or the scrollbar. - _terminal->UserScrollViewport(newValue); + _core->UserScrollViewport(newValue); // User input takes priority over terminal events so cancel // any pending scroll bar update if the user scrolls. @@ -1945,7 +1360,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation 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()) + if (!_autoScrollingPointerPoint.has_value() || + _autoScrollingPointerPoint.value().PointerId() == pointerPoint.PointerId()) { _autoScrollingPointerPoint = pointerPoint; _autoScrollVelocity = scrollVelocity; @@ -1971,7 +1387,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - 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()) + if (_autoScrollingPointerPoint.has_value() && + pointerId == _autoScrollingPointerPoint.value().PointerId()) { _autoScrollingPointerPoint = std::nullopt; _autoScrollVelocity = 0; @@ -1986,8 +1403,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // Method Description: - // - Called continuously to gradually scroll viewport when user is - // mouse selecting outside it (to 'follow' the cursor). + // - 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 */, @@ -2005,9 +1422,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (_autoScrollingPointerPoint.has_value()) { - // Have to take the lock because the renderer will not draw correctly if you move its endpoints while it is generating a frame. - auto lock = _terminal->LockForWriting(); - _SetEndSelectionPointAtCursor(_autoScrollingPointerPoint.value().Position()); } } @@ -2056,7 +1470,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (_cursorTimer.has_value()) { // When the terminal focuses, show the cursor immediately - _terminal->SetCursorOn(true); + _core->CursorOn(true); _cursorTimer.value().Start(); } @@ -2065,7 +1479,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _blinkTimer.value().Start(); } - _UpdateSystemParameterSettings(); + _interactivity->GainFocus(); // Only update the appearance here if an unfocused config exists - // if an unfocused config does not exist then we never would have switched @@ -2076,19 +1490,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - // Method Description - // - Updates internal params based on system parameters - void TermControl::_UpdateSystemParameterSettings() noexcept - { - if (!SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &_rowsToScroll, 0)) - { - LOG_LAST_ERROR(); - // If SystemParametersInfoW fails, which it shouldn't, fall back to - // Windows' default value. - _rowsToScroll = 3; - } - } - // Method Description: // - Event handler for the LostFocus event. This is used to... // - disable accessibility notifications for this TermControl @@ -2118,7 +1519,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (_cursorTimer.has_value()) { _cursorTimer.value().Stop(); - _terminal->SetCursorOn(false); + _core->CursorOn(false); } if (_blinkTimer.has_value()) @@ -2134,125 +1535,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } - // Method Description: - // - Writes the given sequence as input to the active terminal connection. - // - This method has been overloaded to allow zero-copy winrt::param::hstring optimizations. - // Arguments: - // - wstr: the string of characters to write to the terminal connection. - // Return Value: - // - - void TermControl::_SendInputToConnection(const winrt::hstring& wstr) - { - if (_isReadOnly) - { - _RaiseReadOnlyWarning(); - } - else - { - _connection.WriteInput(wstr); - } - } - - void TermControl::_SendInputToConnection(std::wstring_view wstr) - { - if (_isReadOnly) - { - _RaiseReadOnlyWarning(); - } - else - { - _connection.WriteInput(wstr); - } - } - - // Method Description: - // - Pre-process text pasted (presumably from the clipboard) - // before sending it over the terminal's connection. - void TermControl::_SendPastedTextToConnection(const std::wstring& wstr) - { - _terminal->WritePastedText(wstr); - _terminal->ClearSelection(); - _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 _DoResizeUnderLock after this method is called. - // - The write lock should be held when calling this method. - // Arguments: - // - initialUpdate: whether this font update should be considered as being - // concerned with initialization process. Value forwarded to event handler. - void TermControl::_UpdateFont(const bool initialUpdate) - { - 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); - - // If the actual font went through the last-chance fallback routines... - if (_actualFont.GetFallback()) - { - // Then warn the user that we picked something because we couldn't find their font. - - // Format message with user's choice of font and the font that was chosen instead. - const winrt::hstring message{ fmt::format(std::wstring_view{ RS_(L"NoticeFontNotFound") }, _desiredFont.GetFaceName(), _actualFont.GetFaceName()) }; - - // Capture what we need to resume later. - [strongThis = get_strong(), message]() -> winrt::fire_and_forget { - // Take these out of the lambda and store them locally - // because the coroutine will lose them into space - // by the time it resumes. - const auto msg = message; - const auto strong = strongThis; - - // Pop the rest of this function to the tail of the UI thread - // Just in case someone was holding a lock when they called us and - // the handlers decide to do something that take another lock - // (like ShellExecute pumping our messaging thread...GH#7994) - co_await strong->Dispatcher(); - - auto noticeArgs = winrt::make(NoticeLevel::Warning, std::move(msg)); - strong->_RaiseNoticeHandlers(*strong, std::move(noticeArgs)); - }(); - } - - const auto actualNewSize = _actualFont.GetSize(); - _fontSizeChangedHandlers(actualNewSize.X, actualNewSize.Y, initialUpdate); - } - - // 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_cast(fontSize), 1); - const auto fontFace = _settings.FontFace(); - const auto fontWeight = _settings.FontWeight(); - _actualFont = { fontFace, 0, fontWeight.Weight, { 0, newSize }, CP_UTF8, false }; - _desiredFont = { _actualFont }; - - auto lock = _terminal->LockForWriting(); - - // 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?) - _RefreshSizeUnderLock(); - } - CATCH_LOG(); - } - // Method Description: // - Triggered when the swapchain changes size. We use this to resize the // terminal buffers to match the new visible size. @@ -2266,28 +1548,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - auto lock = _terminal->LockForWriting(); - const auto newSize = e.NewSize(); - const auto currentScaleX = SwapChainPanel().CompositionScaleX(); - const auto currentEngineScale = _renderEngine->GetScaling(); - auto foundationSize = newSize; - - // A strange thing can happen here. If you have two tabs open, and drag - // across a DPI boundary, then switch to the other tab, that tab will - // receive two events: First, a SizeChanged, then a ScaleChanged. In the - // SizeChanged event handler, the SwapChainPanel's CompositionScale will - // _already_ be the new scaling, but the engine won't have that value - // yet. If we scale by the CompositionScale here, we'll end up in a - // weird torn state. I'm not totally sure why. - // - // Fortunately we will be getting that following ScaleChanged event, and - // we'll end up resizing again, so we don't terribly need to worry about - // this. - foundationSize.Width *= currentEngineScale; - foundationSize.Height *= currentEngineScale; - - _DoResizeUnderLock(foundationSize.Width, foundationSize.Height); + _core->SizeChanged(newSize.Width, newSize.Height); } // Method Description: @@ -2320,35 +1582,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::_SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, Windows::Foundation::IInspectable const& /*args*/) { - if (_renderEngine) - { - const auto scaleX = sender.CompositionScaleX(); - const auto scaleY = sender.CompositionScaleY(); - const auto dpi = (float)(scaleX * USER_DEFAULT_SCREEN_DPI); - const auto currentEngineScale = _renderEngine->GetScaling(); - - // If we're getting a notification to change to the DPI we already - // have, then we're probably just beginning the DPI change. Since - // we'll get _another_ event with the real DPI, do nothing here for - // now. We'll also skip the next resize in _SwapChainSizeChanged. - const bool dpiWasUnchanged = currentEngineScale == scaleX; - if (dpiWasUnchanged) - { - return; - } - - const auto actualFontOldSize = _actualFont.GetSize(); - - auto lock = _terminal->LockForWriting(); - - _renderer->TriggerFontChange(::base::saturated_cast(dpi), _desiredFont, _actualFont); + const auto scaleX = sender.CompositionScaleX(); - const auto actualFontNewSize = _actualFont.GetSize(); - if (actualFontNewSize != actualFontOldSize) - { - _RefreshSizeUnderLock(); - } - } + _core->ScaleChanged(scaleX); } // Method Description: @@ -2359,11 +1595,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::_CursorTimerTick(Windows::Foundation::IInspectable const& /* sender */, Windows::Foundation::IInspectable const& /* e */) { - if ((_closing) || (!_terminal->IsCursorBlinkingAllowed() && _terminal->IsCursorVisible())) + if (!_closing) { - return; + _core->BlinkCursor(); } - _terminal->SetCursorOn(!_terminal->IsCursorOn()); } // Method Description: @@ -2376,9 +1611,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (!_closing) { - auto& renderTarget = *_renderer; - auto& blinkingState = _terminal->GetBlinkingState(); - blinkingState.ToggleBlinkingRendition(renderTarget); + _core->BlinkAttributeTick(); } } @@ -2388,110 +1621,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - cursorPosition: in pixels, relative to the origin of the control void TermControl::_SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition) { - if (!_terminal->IsSelectionActive()) - { - return; - } - - 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->SetSelectionEnd(terminalPosition); - _renderer->TriggerSelection(); - _selectionNeedsToBeCopied = true; - } - - // Method Description: - // - Perform a resize for the current size of the swapchainpanel. If the - // font size changed, we'll need to resize the buffer to fit the existing - // swapchain size. This helper will call _DoResizeUnderLock with the current size - // of the swapchain, accounting for scaling due to DPI. - // - Note that a DPI change will also trigger a font size change, and will call into here. - // - The write lock should be held when calling this method, we might be changing the buffer size in _DoResizeUnderLock. - // Arguments: - // - - // Return Value: - // - - void TermControl::_RefreshSizeUnderLock() - { - const auto currentScaleX = SwapChainPanel().CompositionScaleX(); - const auto currentScaleY = SwapChainPanel().CompositionScaleY(); - const auto actualWidth = SwapChainPanel().ActualWidth(); - const auto actualHeight = SwapChainPanel().ActualHeight(); - - const auto widthInPixels = actualWidth * currentScaleX; - const auto heightInPixels = actualHeight * currentScaleY; - - _DoResizeUnderLock(widthInPixels, heightInPixels); - } - - // 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::_DoResizeUnderLock(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; - } - - _terminal->ClearSelection(); - - // 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. - const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() }); - if (SUCCEEDED(hr) && hr != S_FALSE) - { - _connection.Resize(vp.Height(), vp.Width()); - } - } - - void TermControl::_TerminalWarningBell() - { - _WarningBellHandlers(*this, nullptr); - } - - void TermControl::_TerminalTitleChanged(const std::wstring_view& wstr) - { - _TitleChangedHandlers(*this, winrt::make(winrt::hstring{ wstr })); - } - void TermControl::_TerminalTabColorChanged(const std::optional /*color*/) - { - _TabColorChangedHandlers(*this, nullptr); - } - - void TermControl::_CopyToClipboard(const std::wstring_view& wstr) - { - auto copyArgs = winrt::make_self(winrt::hstring(wstr)); - _CopyToClipboardHandlers(*this, *copyArgs); + _interactivity->SetEndSelectionPoint(_toTerminalOrigin(cursorPosition)); } // Method Description: @@ -2504,9 +1634,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // 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) + void TermControl::_ScrollPositionChanged(const IInspectable& /*sender*/, + const Control::ScrollPositionChangedArgs& args) { // Since this callback fires from non-UI thread, we might be already // closed/closing. @@ -2515,20 +1644,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - // Clear the regex pattern tree so the renderer does not try to render them while scrolling - // We're **NOT** taking the lock here unlike _ScrollbarChangeHandler because - // we are already under lock (since this usually happens as a result of writing). - // TODO GH#9617: refine locking around pattern tree - _terminal->ClearPatternTree(); - - _scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize); - ScrollBarUpdate update; - const auto hiddenContent = bufferSize - viewHeight; + const auto hiddenContent = args.BufferSize() - args.ViewHeight(); update.newMaximum = hiddenContent; update.newMinimum = 0; - update.newViewportSize = viewHeight; - update.newValue = viewTop; + update.newViewportSize = args.ViewHeight(); + update.newValue = args.ViewTop(); _updateScrollBar->Run(update); _updatePatternLocations->Run(); @@ -2539,15 +1660,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation // to be where the current cursor position is. // Arguments: // - N/A - void TermControl::_TerminalCursorPositionChanged() + void TermControl::_CursorPositionChanged(const IInspectable& /*sender*/, + const IInspectable& /*args*/) { _tsfTryRedrawCanvas->Run(); } hstring TermControl::Title() { - hstring hstr{ _terminal->GetConsoleTitle() }; - return hstr; + return _core->Title(); } hstring TermControl::GetProfileName() const @@ -2557,13 +1678,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation hstring TermControl::WorkingDirectory() const { - hstring hstr{ _terminal->GetWorkingDirectory() }; - return hstr; + return _core->WorkingDirectory(); } bool TermControl::BracketedPasteEnabled() const noexcept { - return _terminal->IsXtermBracketedPasteModeEnabled(); + return _core->BracketedPasteEnabled(); } // Method Description: @@ -2581,135 +1701,29 @@ namespace winrt::Microsoft::Terminal::Control::implementation return false; } - // no selection --> nothing to copy - if (!_terminal->IsSelectionActive()) - { - return false; - } - - // Mark the current selection as copied - _selectionNeedsToBeCopied = false; - - // extract text from buffer - const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(singleLine); - - // convert text: vector --> string - std::wstring textData; - for (const auto& text : bufferData.text) - { - textData += text; - } - - // convert text to HTML format - // GH#5347 - Don't provide a title for the generated HTML, as many - // web applications will paste the title first, followed by the HTML - // content, which is unexpected. - const auto htmlData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::HTML) ? - TextBuffer::GenHTML(bufferData, - _actualFont.GetUnscaledSize().Y, - _actualFont.GetFaceName(), - til::color{ _settings.DefaultBackground() }) : - ""; - - // convert to RTF format - const auto rtfData = formats == nullptr || WI_IsFlagSet(formats.Value(), CopyFormat::RTF) ? - TextBuffer::GenRTF(bufferData, - _actualFont.GetUnscaledSize().Y, - _actualFont.GetFaceName(), - til::color{ _settings.DefaultBackground() }) : - ""; - - if (!_settings.CopyOnSelect()) - { - _terminal->ClearSelection(); - _renderer->TriggerSelection(); - } - - // send data up for clipboard - auto copyArgs = winrt::make_self(winrt::hstring(textData), - winrt::to_hstring(htmlData), - winrt::to_hstring(rtfData), - formats); - _CopyToClipboardHandlers(*this, *copyArgs); - return true; + return _interactivity->CopySelectionToClipboard(singleLine, formats); } // 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 - _PasteFromClipboardHandlers(*this, *pasteArgs); - } - - // Method Description: - // - Asynchronously close our connection. The Connection will likely wait - // until the attached process terminates before Close returns. If that's - // the case, we don't want to block the UI thread waiting on that process - // handle. - // Arguments: - // - - // Return Value: - // - - winrt::fire_and_forget TermControl::_AsyncCloseConnection() - { - if (auto localConnection{ std::exchange(_connection, nullptr) }) - { - // Close the connection on the background thread. - co_await winrt::resume_background(); - localConnection.Close(); - // connection is destroyed. - } + _interactivity->RequestPasteTextFromClipboard(); } void TermControl::Close() { if (!_closing.exchange(true)) { - _RestorePointerCursorHandlers(*this, nullptr); + _core->ReceivedOutput(_coreOutputEventToken); - // Stop accepting new output and state changes before we disconnect everything. - _connection.TerminalOutput(_connectionOutputEventToken); - _connectionStateChangedRevoker.revoke(); + _RestorePointerCursorHandlers(*this, nullptr); - TSFInputControl().Close(); // Disconnect the TSF input control so it doesn't receive EditContext events. + // Disconnect the TSF input control so it doesn't receive EditContext events. + TSFInputControl().Close(); _autoScrollTimer.Stop(); - // GH#1996 - Close the connection asynchronously on a background - // thread. - // Since TermControl::Close is only ever triggered by the UI, we - // don't really care to wait for the connection to be completely - // closed. We can just do it whenever. - _AsyncCloseConnection(); - - { - // GH#8734: - // We lock the terminal here to make sure it isn't still being - // used in the connection thread before we destroy the renderer. - // However, we must unlock it again prior to triggering the - // teardown, to avoid the render thread being deadlocked. The - // renderer may be waiting to acquire the terminal lock, while - // we're waiting for the renderer to finish. - auto lock = _terminal->LockForWriting(); - } - - if (auto localRenderEngine{ std::exchange(_renderEngine, nullptr) }) - { - if (auto localRenderer{ std::exchange(_renderer, nullptr) }) - { - localRenderer->TriggerTeardown(); - // renderer is destroyed - } - // renderEngine is destroyed - } - - // we don't destroy _terminal here; it now has the same lifetime as the - // control. + _core->Close(); } } @@ -2722,19 +1736,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation ScrollBar().Value(viewTop); } - int TermControl::GetScrollOffset() + int TermControl::ScrollOffset() { - return _terminal->GetScrollOffset(); + return _core->ScrollOffset(); } // 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 + int TermControl::ViewHeight() const { - const auto viewPort = _terminal->GetViewport(); - return viewPort.Height(); + return _core->ViewHeight(); } // Function Description: @@ -2851,7 +1864,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The dimensions of a single character of this control, in DIPs winrt::Windows::Foundation::Size TermControl::CharacterDimensions() const { - const auto fontSize = _actualFont.GetSize(); + const auto fontSize = _core->GetFont().GetSize(); return { gsl::narrow_cast(fontSize.X), gsl::narrow_cast(fontSize.Y) }; } @@ -2868,7 +1881,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { if (_initializedTerminal) { - const auto fontSize = _actualFont.GetSize(); + const auto fontSize = _core->GetFont().GetSize(); double width = fontSize.X; double height = fontSize.Y; // Reserve additional space if scrollbar is intended to be visible @@ -2913,7 +1926,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - A dimension that would be aligned to the character grid. float TermControl::SnapDimensionToGrid(const bool widthOrHeight, const float dimension) { - const auto fontSize = _actualFont.GetSize(); + const auto fontSize = _core->GetFont().GetSize(); const auto fontDimension = widthOrHeight ? fontSize.X : fontSize.Y; const auto padding = GetPadding(); @@ -3038,15 +2051,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // 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. + // - Gets the corresponding viewport pixel position for the cursor + // by excluding the padding. // 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) + // - the corresponding viewport terminal position (in pixels) for the given Point parameter + const til::point TermControl::_toTerminalOrigin(winrt::Windows::Foundation::Point cursorPosition) { // cursorPosition is DIPs, relative to SwapChainPanel origin const til::point cursorPosInDIPs{ til::math::rounding, cursorPosition }; @@ -3058,11 +2070,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // Convert it to pixels const til::point relativeToMarginInPixels{ relativeToMarginInDIPs * SwapChainPanel().CompositionScaleX() }; - // Get the size of the font, which is in pixels - const til::size fontSize{ _actualFont.GetSize() }; - - // Convert the location in pixels to characters within the current viewport. - return til::point{ relativeToMarginInPixels / fontSize }; + return relativeToMarginInPixels; } // Method Description: @@ -3079,7 +2087,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - _connection.WriteInput(text); + _core->SendInput(text); } // Method Description: @@ -3089,9 +2097,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - eventArgs: event for storing the current cursor position // Return Value: // - - void TermControl::_CurrentCursorPositionHandler(const IInspectable& /*sender*/, const CursorPositionEventArgs& eventArgs) + void TermControl::_CurrentCursorPositionHandler(const IInspectable& /*sender*/, + const CursorPositionEventArgs& eventArgs) { - auto lock = _terminal->LockForReading(); if (!_initializedTerminal) { // fake it @@ -3099,8 +2107,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation return; } - const til::point cursorPos = _terminal->GetCursorPosition(); - Windows::Foundation::Point p = { ::base::ClampedNumeric(cursorPos.x()), ::base::ClampedNumeric(cursorPos.y()) }; + const til::point cursorPos = _core->CursorPosition(); + Windows::Foundation::Point p = { ::base::ClampedNumeric(cursorPos.x()), + ::base::ClampedNumeric(cursorPos.y()) }; eventArgs.CurrentPosition(p); } @@ -3111,44 +2120,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - eventArgs: event for storing the current font information // Return Value: // - - void TermControl::_FontInfoHandler(const IInspectable& /*sender*/, const FontInfoEventArgs& eventArgs) + void TermControl::_FontInfoHandler(const IInspectable& /*sender*/, + const FontInfoEventArgs& eventArgs) { + const auto fontInfo = _core->GetFont(); eventArgs.FontSize(CharacterDimensions()); - eventArgs.FontFace(_actualFont.GetFaceName()); + eventArgs.FontFace(fontInfo.GetFaceName()); ::winrt::Windows::UI::Text::FontWeight weight; - weight.Weight = static_cast(_actualFont.GetWeight()); + weight.Weight = static_cast(fontInfo.GetWeight()); eventArgs.FontWeight(weight); } - // Method Description: - // - Returns the number of clicks that occurred (double and triple click support). - // Every call to this function registers a click. - // 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, _lastMouseClickTimestamp, &delta)); - if (clickPos != _lastMouseClickPos || delta > _multiClickTimer) - { - _multiClickCounter = 1; - } - else - { - _multiClickCounter++; - } - - _lastMouseClickTimestamp = clickTime; - _lastMouseClickPos = clickPos; - return _multiClickCounter; - } - // Method Description: // - Calculates speed of single axis of auto scrolling. It has to allow for both // fast and precise selection. @@ -3174,8 +2156,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - e: The DragEventArgs from the Drop event // Return Value: // - - winrt::fire_and_forget TermControl::_DragDropHandler(Windows::Foundation::IInspectable const& /*sender*/, - DragEventArgs const e) + winrt::fire_and_forget TermControl::_DragDropHandler(Windows::Foundation::IInspectable /*sender*/, + DragEventArgs e) { if (_closing) { @@ -3187,7 +2169,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation try { Windows::Foundation::Uri link{ co_await e.DataView().GetApplicationLinkAsync() }; - _SendPastedTextToConnection(std::wstring{ link.AbsoluteUri() }); + _core->PasteText(link.AbsoluteUri()); } CATCH_LOG(); } @@ -3196,7 +2178,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation try { Windows::Foundation::Uri link{ co_await e.DataView().GetWebLinkAsync() }; - _SendPastedTextToConnection(std::wstring{ link.AbsoluteUri() }); + _core->PasteText(link.AbsoluteUri()); } CATCH_LOG(); } @@ -3204,8 +2186,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { try { - std::wstring text{ co_await e.DataView().GetTextAsync() }; - _SendPastedTextToConnection(text); + auto text{ co_await e.DataView().GetTextAsync() }; + _core->PasteText(text); } CATCH_LOG(); } @@ -3238,8 +2220,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation fullPath.end(), L' ') != fullPath.end(); - auto lock = _terminal->LockForWriting(); - if (containsSpaces) { fullPath.insert(0, L"\""); @@ -3248,7 +2228,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation allPaths += fullPath; } - _SendInputToConnection(allPaths); + _core->PasteText(winrt::hstring{ allPaths }); } } } @@ -3305,10 +2285,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - Checks if the uri is valid and sends an event if so // Arguments: // - The uri - winrt::fire_and_forget TermControl::_HyperlinkHandler(const std::wstring_view uri) + winrt::fire_and_forget TermControl::_HyperlinkHandler(IInspectable /*sender*/, + Control::OpenHyperlinkEventArgs args) { // Save things we need to resume later. - winrt::hstring heldUri{ uri }; auto strongThis{ get_strong() }; // Pop the rest of this function to the tail of the UI thread @@ -3317,13 +2297,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation // (like ShellExecute pumping our messaging thread...GH#7994) co_await Dispatcher(); - auto hyperlinkArgs = winrt::make_self(heldUri); - _OpenHyperlinkHandlers(*strongThis, *hyperlinkArgs); + _OpenHyperlinkHandlers(*strongThis, args); } // Method Description: // - Produces the error dialog that notifies the user that rendering cannot proceed. - winrt::fire_and_forget TermControl::_RendererEnteredErrorState() + winrt::fire_and_forget TermControl::_RendererEnteredErrorState(IInspectable /*sender*/, + IInspectable /*args*/) { auto strongThis{ get_strong() }; co_await Dispatcher(); // pop up onto the UI thread @@ -3343,7 +2323,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { // It's already loaded if we get here, so just hide it. RendererFailedNotice().Visibility(Visibility::Collapsed); - _renderer->ResetErrorStateAndResume(); + _core->ResumeRendering(); } IControlSettings TermControl::Settings() const @@ -3353,17 +2333,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::IReference TermControl::TabColor() noexcept { - auto coreColor = _terminal->GetTabColor(); - return coreColor.has_value() ? Windows::Foundation::IReference(til::color{ coreColor.value() }) : nullptr; - } - - // Method Description: - // - Sends an event (which will be caught by TerminalPage and forwarded to AppHost after) - // to set the progress indicator on the taskbar - winrt::fire_and_forget TermControl::TaskbarProgressChanged() - { - co_await resume_foreground(Dispatcher(), CoreDispatcherPriority::High); - _SetTaskbarProgressHandlers(*this, nullptr); + // NOTE TO FUTURE READERS: TabColor is down in the Core for the + // hypothetical future where we allow an application to set the tab + // color with VT sequences like they're currently allowed to with the + // title. + return _core->TabColor(); } // Method Description: @@ -3372,7 +2346,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The taskbar state of this control const size_t TermControl::TaskbarState() const noexcept { - return _terminal->GetTaskbarState(); + return _core->TaskbarState(); } // Method Description: @@ -3381,7 +2355,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - The taskbar progress of this control const size_t TermControl::TaskbarProgress() const noexcept { - return _terminal->GetTaskbarProgress(); + return _core->TaskbarProgress(); } // Method Description: @@ -3390,106 +2364,132 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - True if the mode is read-only bool TermControl::ReadOnly() const noexcept { - return _isReadOnly; + return _core->IsInReadOnlyMode(); } // Method Description: // - Toggles the read-only flag, raises event describing the value change void TermControl::ToggleReadOnly() { - _isReadOnly = !_isReadOnly; - _ReadOnlyChangedHandlers(*this, winrt::box_value(_isReadOnly)); + _core->ToggleReadOnlyMode(); + _ReadOnlyChangedHandlers(*this, winrt::box_value(_core->IsInReadOnlyMode())); } - winrt::fire_and_forget TermControl::_RaiseReadOnlyWarning() + // Method Description: + // - Handle a mouse exited event, specifically clearing last hovered cell + // and removing selection from hyper link if exists + // Arguments: + // - sender: not used + // - args: event data + void TermControl::_PointerExitedHandler(Windows::Foundation::IInspectable const& /*sender*/, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& /*e*/) { - auto weakThis{ get_weak() }; - co_await winrt::resume_foreground(Dispatcher()); - - if (auto control{ weakThis.get() }) - { - auto noticeArgs = winrt::make(NoticeLevel::Info, RS_(L"TermControlReadOnly")); - control->_RaiseNoticeHandlers(*control, std::move(noticeArgs)); - } + _core->UpdateHoveredCell(std::nullopt); } - // Method description: - // - Updates last hovered cell, renders / removes rendering of hyper-link if required - // Arguments: - // - terminalPosition: The terminal position of the pointer - void TermControl::_UpdateHoveredCell(const std::optional& terminalPosition) + winrt::fire_and_forget TermControl::_hoveredHyperlinkChanged(IInspectable sender, + IInspectable args) { - if (terminalPosition == _lastHoveredCell) + auto weakThis{ get_weak() }; + co_await resume_foreground(Dispatcher()); + if (auto self{ weakThis.get() }) { - return; + auto lastHoveredCell = _core->GetHoveredCell(); + if (lastHoveredCell.has_value()) + { + const auto uriText = _core->GetHoveredUriText(); + if (!uriText.empty()) + { + // Update the tooltip with the URI + HoveredUri().Text(uriText); + + // Set the border thickness so it covers the entire cell + const auto charSizeInPixels = CharacterDimensions(); + const auto htInDips = charSizeInPixels.Height / SwapChainPanel().CompositionScaleY(); + const auto wtInDips = charSizeInPixels.Width / SwapChainPanel().CompositionScaleX(); + const Thickness newThickness{ wtInDips, htInDips, 0, 0 }; + HyperlinkTooltipBorder().BorderThickness(newThickness); + + // Compute the location of the top left corner of the cell in DIPS + const til::size marginsInDips{ til::math::rounding, GetPadding().Left, GetPadding().Top }; + const til::point startPos{ *lastHoveredCell }; + const til::size fontSize{ _core->GetFont().GetSize() }; + const til::point posInPixels{ startPos * fontSize }; + const til::point posInDIPs{ posInPixels / SwapChainPanel().CompositionScaleX() }; + const til::point locationInDIPs{ posInDIPs + marginsInDips }; + + // Move the border to the top left corner of the cell + OverlayCanvas().SetLeft(HyperlinkTooltipBorder(), + (locationInDIPs.x() - SwapChainPanel().ActualOffset().x)); + OverlayCanvas().SetTop(HyperlinkTooltipBorder(), + (locationInDIPs.y() - SwapChainPanel().ActualOffset().y)); + } + } } + } - _lastHoveredCell = terminalPosition; + void TermControl::_coreFontSizeChanged(const int fontWidth, + const int fontHeight, + const bool isInitialChange) + { + // Don't try to inspect the core here. The Core is raising this while + // it's holding its write lock. If the handlers calls back to some + // method on the TermControl on the same thread, and that _method_ calls + // to ControlCore, we might be in danger of deadlocking. + _FontSizeChangedHandlers(fontWidth, fontHeight, isInitialChange); + } - uint16_t newId{ 0u }; - // we can't use auto here because we're pre-declaring newInterval. - decltype(_terminal->GetHyperlinkIntervalFromPosition(COORD{})) newInterval{ std::nullopt }; - if (terminalPosition.has_value()) - { - auto lock = _terminal->LockForReading(); // Lock for the duration of our reads. + void TermControl::_coreRaisedNotice(const IInspectable& /*sender*/, + const Control::NoticeEventArgs& eventArgs) + { + // Don't try to inspect the core here. The Core might be raising this + // while it's holding its write lock. If the handlers calls back to some + // method on the TermControl on the same thread, and _that_ method calls + // to ControlCore, we might be in danger of deadlocking. + _RaiseNoticeHandlers(*this, eventArgs); + } - const auto uri = _terminal->GetHyperlinkAtPosition(*terminalPosition); - if (!uri.empty()) - { - // Update the tooltip with the URI - HoveredUri().Text(uri); - - // Set the border thickness so it covers the entire cell - const auto charSizeInPixels = CharacterDimensions(); - const auto htInDips = charSizeInPixels.Height / SwapChainPanel().CompositionScaleY(); - const auto wtInDips = charSizeInPixels.Width / SwapChainPanel().CompositionScaleX(); - const Thickness newThickness{ wtInDips, htInDips, 0, 0 }; - HyperlinkTooltipBorder().BorderThickness(newThickness); - - // Compute the location of the top left corner of the cell in DIPS - const til::size marginsInDips{ til::math::rounding, GetPadding().Left, GetPadding().Top }; - const til::point startPos{ terminalPosition->X, terminalPosition->Y }; - const til::size fontSize{ _actualFont.GetSize() }; - const til::point posInPixels{ startPos * fontSize }; - const til::point posInDIPs{ posInPixels / SwapChainPanel().CompositionScaleX() }; - const til::point locationInDIPs{ posInDIPs + marginsInDips }; - - // Move the border to the top left corner of the cell - OverlayCanvas().SetLeft(HyperlinkTooltipBorder(), (locationInDIPs.x() - SwapChainPanel().ActualOffset().x)); - OverlayCanvas().SetTop(HyperlinkTooltipBorder(), (locationInDIPs.y() - SwapChainPanel().ActualOffset().y)); - } + TerminalInput::MouseButtonState TermControl::GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point) + { + return TerminalInput::MouseButtonState{ point.Properties().IsLeftButtonPressed(), + point.Properties().IsMiddleButtonPressed(), + point.Properties().IsRightButtonPressed() }; + } - newId = _terminal->GetHyperlinkIdAtPosition(*terminalPosition); - newInterval = _terminal->GetHyperlinkIntervalFromPosition(*terminalPosition); - } + unsigned int TermControl::GetPointerUpdateKind(const winrt::Windows::UI::Input::PointerPoint point) + { + const auto props = point.Properties(); - // If the hyperlink ID changed or the interval changed, trigger a redraw all - // (so this will happen both when we move onto a link and when we move off a link) - if (newId != _lastHoveredId || (newInterval != _lastHoveredInterval)) + // Which mouse button changed state (and how) + unsigned int uiButton{}; + switch (props.PointerUpdateKind()) { - auto lock = _terminal->LockForWriting(); - _lastHoveredId = newId; - _lastHoveredInterval = newInterval; - _renderEngine->UpdateHyperlinkHoveredId(newId); - _renderer->UpdateLastHoveredInterval(newInterval); - _renderer->TriggerRedrawAll(); + case winrt::Windows::UI::Input::PointerUpdateKind::LeftButtonPressed: + uiButton = WM_LBUTTONDOWN; + break; + case winrt::Windows::UI::Input::PointerUpdateKind::LeftButtonReleased: + uiButton = WM_LBUTTONUP; + break; + case winrt::Windows::UI::Input::PointerUpdateKind::MiddleButtonPressed: + uiButton = WM_MBUTTONDOWN; + break; + case winrt::Windows::UI::Input::PointerUpdateKind::MiddleButtonReleased: + uiButton = WM_MBUTTONUP; + break; + case winrt::Windows::UI::Input::PointerUpdateKind::RightButtonPressed: + uiButton = WM_RBUTTONDOWN; + break; + case winrt::Windows::UI::Input::PointerUpdateKind::RightButtonReleased: + uiButton = WM_RBUTTONUP; + break; + default: + uiButton = WM_MOUSEMOVE; } + + return uiButton; } - // Method Description: - // - Handle a mouse exited event, specifically clearing last hovered cell - // and removing selection from hyper link if exists - // Arguments: - // - sender: not used - // - args: event data - void TermControl::_PointerExitedHandler(Windows::Foundation::IInspectable const& /*sender*/, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& /*e*/) + void TermControl::_coreWarningBell(const IInspectable& /*sender*/, const IInspectable& /*args*/) { - _UpdateHoveredCell(std::nullopt); + _playWarningBell->Run(); } - - // -------------------------------- 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, FontSizeChanged, _fontSizeChangedHandlers, Control::FontSizeChangedEventArgs); - DEFINE_EVENT(TermControl, ScrollPositionChanged, _scrollPositionChangedHandlers, Control::ScrollPositionChangedEventArgs); } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 0ebbc9186e2..54fd49c37f2 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -14,6 +14,8 @@ #include "SearchBoxControl.h" #include "ThrottledFunc.h" +#include "ControlInteractivity.h" + namespace Microsoft::Console::VirtualTerminal { struct MouseButtonState; @@ -28,11 +30,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::fire_and_forget UpdateSettings(); winrt::fire_and_forget UpdateAppearance(const IControlAppearance newAppearance); - hstring Title(); hstring GetProfileName() const; - hstring WorkingDirectory() const; - bool BracketedPasteEnabled() const noexcept; bool CopySelectionToClipboard(bool singleLine, const Windows::Foundation::IReference& formats); void PasteTextFromClipboard(); void Close(); @@ -40,21 +39,39 @@ namespace winrt::Microsoft::Terminal::Control::implementation Windows::Foundation::Size MinimumSize(); float SnapDimensionToGrid(const bool widthOrHeight, const float dimension); +#pragma region ICoreState + const size_t TaskbarState() const noexcept; + const size_t TaskbarProgress() const noexcept; + + hstring Title(); + Windows::Foundation::IReference TabColor() noexcept; + hstring WorkingDirectory() const; + + TerminalConnection::ConnectionState ConnectionState() const; + + int ScrollOffset(); + int ViewHeight() const; + int BufferHeight() const; + + bool BracketedPasteEnabled() const noexcept; +#pragma endregion + void ScrollViewport(int viewTop); - int GetScrollOffset(); - int GetViewHeight() const; void AdjustFontSize(int fontSizeDelta); void ResetFontSize(); + til::point GetFontSize() const; void SendInput(const winrt::hstring& input); void ToggleShaderEffects(); - winrt::fire_and_forget RenderEngineSwapChainChanged(); + winrt::fire_and_forget RenderEngineSwapChainChanged(IInspectable sender, IInspectable args); void _AttachDxgiSwapChainToXaml(IDXGISwapChain1* swapChain); - winrt::fire_and_forget _RendererEnteredErrorState(); + winrt::fire_and_forget _RendererEnteredErrorState(IInspectable sender, IInspectable args); + void _RenderRetryButton_Click(IInspectable const& button, IInspectable const& args); - winrt::fire_and_forget _RendererWarning(const HRESULT hr); + winrt::fire_and_forget _RendererWarning(IInspectable sender, + Control::RendererWarningArgs args); void CreateSearchBoxControl(); @@ -64,16 +81,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool OnMouseWheel(const Windows::Foundation::Point location, const int32_t delta, const bool leftButtonDown, const bool midButtonDown, const bool rightButtonDown); - void UpdatePatternLocations(); - ~TermControl(); Windows::UI::Xaml::Automation::Peers::AutomationPeer OnCreateAutomationPeer(); ::Microsoft::Console::Types::IUiaData* GetUiaData() const; - const FontInfo GetActualFont() const; const Windows::UI::Xaml::Thickness GetPadding(); - TerminalConnection::ConnectionState ConnectionState() const; IControlSettings Settings() const; static Windows::Foundation::Size GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi); @@ -85,64 +98,52 @@ namespace winrt::Microsoft::Terminal::Control::implementation const winrt::hstring& padding, const uint32_t dpi); - Windows::Foundation::IReference TabColor() noexcept; - - winrt::fire_and_forget TaskbarProgressChanged(); - const size_t TaskbarState() const noexcept; - const size_t TaskbarProgress() const noexcept; - bool ReadOnly() const noexcept; void ToggleReadOnly(); - // clang-format off + static ::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point); + static unsigned int GetPointerUpdateKind(const winrt::Windows::UI::Input::PointerPoint point); + // -------------------------------- WinRT Events --------------------------------- - DECLARE_EVENT(FontSizeChanged, _fontSizeChangedHandlers, Control::FontSizeChangedEventArgs); - DECLARE_EVENT(ScrollPositionChanged, _scrollPositionChangedHandlers, Control::ScrollPositionChangedEventArgs); - - TYPED_EVENT(TitleChanged, IInspectable, Control::TitleChangedEventArgs); - TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs); - TYPED_EVENT(CopyToClipboard, IInspectable, Control::CopyToClipboardEventArgs); - TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); - TYPED_EVENT(SetTaskbarProgress, IInspectable, IInspectable); - TYPED_EVENT(RaiseNotice, IInspectable, Control::NoticeEventArgs); - - TYPED_EVENT(WarningBell, IInspectable, IInspectable); - TYPED_EVENT(ConnectionStateChanged, IInspectable, IInspectable); - TYPED_EVENT(Initialized, Control::TermControl, Windows::UI::Xaml::RoutedEventArgs); - TYPED_EVENT(TabColorChanged, IInspectable, IInspectable); - TYPED_EVENT(HidePointerCursor, IInspectable, IInspectable); - TYPED_EVENT(RestorePointerCursor, IInspectable, IInspectable); - TYPED_EVENT(ReadOnlyChanged, IInspectable, IInspectable); + // clang-format off + WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); + + FORWARDED_TYPED_EVENT(CopyToClipboard, IInspectable, Control::CopyToClipboardEventArgs, _core, CopyToClipboard); + FORWARDED_TYPED_EVENT(TitleChanged, IInspectable, Control::TitleChangedEventArgs, _core, TitleChanged); + FORWARDED_TYPED_EVENT(TabColorChanged, IInspectable, IInspectable, _core, TabColorChanged); + FORWARDED_TYPED_EVENT(SetTaskbarProgress, IInspectable, IInspectable, _core, TaskbarProgressChanged); + FORWARDED_TYPED_EVENT(ConnectionStateChanged, IInspectable, IInspectable, _core, ConnectionStateChanged); + FORWARDED_TYPED_EVENT(PasteFromClipboard, IInspectable, Control::PasteFromClipboardEventArgs, _interactivity, PasteFromClipboard); + + TYPED_EVENT(OpenHyperlink, IInspectable, Control::OpenHyperlinkEventArgs); + TYPED_EVENT(RaiseNotice, IInspectable, Control::NoticeEventArgs); + TYPED_EVENT(HidePointerCursor, IInspectable, IInspectable); + TYPED_EVENT(RestorePointerCursor, IInspectable, IInspectable); + TYPED_EVENT(ReadOnlyChanged, IInspectable, IInspectable); TYPED_EVENT(FocusFollowMouseRequested, IInspectable, IInspectable); + TYPED_EVENT(Initialized, Control::TermControl, Windows::UI::Xaml::RoutedEventArgs); + TYPED_EVENT(WarningBell, IInspectable, IInspectable); // clang-format on WINRT_PROPERTY(IControlAppearance, UnfocusedAppearance); private: friend struct TermControlT; // friend our parent so it can bind private event handlers - TerminalConnection::ITerminalConnection _connection; - bool _initializedTerminal; - winrt::com_ptr _searchBox; + winrt::com_ptr _core; + winrt::com_ptr _interactivity; - event_token _connectionOutputEventToken; - TerminalConnection::ITerminalConnection::StateChanged_revoker _connectionStateChangedRevoker; + bool _initializedTerminal; - std::unique_ptr<::Microsoft::Terminal::Core::Terminal> _terminal; + winrt::com_ptr _searchBox; - std::unique_ptr<::Microsoft::Console::Render::Renderer> _renderer; - std::unique_ptr<::Microsoft::Console::Render::DxEngine> _renderEngine; std::unique_ptr<::Microsoft::Console::Render::UiaEngine> _uiaEngine; IControlSettings _settings; bool _focused; std::atomic _closing; - FontInfoDesired _desiredFont; - FontInfo _actualFont; - std::shared_ptr> _tsfTryRedrawCanvas; - std::shared_ptr> _updatePatternLocations; std::shared_ptr> _playWarningBell; @@ -157,59 +158,28 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::shared_ptr> _updateScrollBar; bool _isInternalScrollBarUpdate; - unsigned int _rowsToScroll; - // Auto scroll occurs when user, while selecting, drags cursor outside viewport. View is then scrolled to 'follow' the cursor. double _autoScrollVelocity; std::optional _autoScrollingPointerPoint; Windows::UI::Xaml::DispatcherTimer _autoScrollTimer; std::optional _lastAutoScrollUpdateTime; - // storage location for the leading surrogate of a utf-16 surrogate pair - std::optional _leadingSurrogate; - std::optional _cursorTimer; std::optional _blinkTimer; - // If this is set, then we assume we are in the middle of panning the - // viewport via touch input. - std::optional _touchAnchor; - - // Track the last cell we hovered over (used in pointerMovedHandler) - std::optional _lastHoveredCell; - // Track the last hyperlink ID we hovered over - uint16_t _lastHoveredId; - - std::optional::interval> _lastHoveredInterval; - - using Timestamp = uint64_t; - - // imported from WinUser - // Used for PointerPoint.Timestamp Property (https://docs.microsoft.com/en-us/uwp/api/windows.ui.input.pointerpoint.timestamp#Windows_UI_Input_PointerPoint_Timestamp) - Timestamp _multiClickTimer; - unsigned int _multiClickCounter; - Timestamp _lastMouseClickTimestamp; - std::optional _lastMouseClickPos; - std::optional _lastMouseClickPosNoSelection; - std::optional _singleClickTouchdownPos; - // This field tracks whether the selection has changed meaningfully - // since it was last copied. It's generally used to prevent copyOnSelect - // from firing when the pointer _just happens_ to be released over the - // terminal. - bool _selectionNeedsToBeCopied; + event_token _coreOutputEventToken; winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker; - void _UpdateSettingsFromUIThreadUnderLock(IControlSettings newSettings); - void _UpdateAppearanceFromUIThreadUnderLock(IControlAppearance newAppearance); - bool _isReadOnly{ false }; - + void _UpdateSettingsFromUIThread(IControlSettings newSettings); + void _UpdateAppearanceFromUIThread(IControlAppearance newAppearance); void _ApplyUISettings(const IControlSettings&); - void _UpdateSystemParameterSettings() noexcept; + void _InitializeBackgroundBrush(); - winrt::fire_and_forget _BackgroundColorChanged(const til::color color); + void _BackgroundColorChangedHandler(const IInspectable& sender, const IInspectable& args); + winrt::fire_and_forget _changeBackgroundColor(const til::color bg); + bool _InitializeTerminal(); - void _UpdateFont(const bool initialUpdate = false); void _SetFontSize(int fontSize); void _TappedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::TappedRoutedEventArgs const& e); void _KeyDownHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e); @@ -221,33 +191,27 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _PointerExitedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); void _MouseWheelHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); void _ScrollbarChangeHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Controls::Primitives::RangeBaseValueChangedEventArgs const& e); + void _GotFocusHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e); void _LostFocusHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e); - winrt::fire_and_forget _DragDropHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::DragEventArgs const e); + + winrt::fire_and_forget _DragDropHandler(Windows::Foundation::IInspectable sender, Windows::UI::Xaml::DragEventArgs e); void _DragOverHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::DragEventArgs const& e); - winrt::fire_and_forget _HyperlinkHandler(const std::wstring_view uri); + + winrt::fire_and_forget _HyperlinkHandler(Windows::Foundation::IInspectable sender, Control::OpenHyperlinkEventArgs e); void _CursorTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); void _BlinkTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e); + void _SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition); - void _SendInputToConnection(const winrt::hstring& wstr); - void _SendInputToConnection(std::wstring_view wstr); - void _SendPastedTextToConnection(const std::wstring& wstr); + void _SwapChainSizeChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::SizeChangedEventArgs const& e); void _SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, Windows::Foundation::IInspectable const& args); - void _DoResizeUnderLock(const double newWidth, const double newHeight); - void _RefreshSizeUnderLock(); - void _TerminalWarningBell(); - void _TerminalTitleChanged(const std::wstring_view& wstr); + void _TerminalTabColorChanged(const std::optional color); - void _CopyToClipboard(const std::wstring_view& wstr); - void _TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize); - void _TerminalCursorPositionChanged(); - void _MouseScrollHandler(const double mouseDelta, const Windows::Foundation::Point point, const bool isLeftButtonPressed); - void _MouseZoomHandler(const double delta); - void _MouseTransparencyHandler(const double delta); - bool _DoMouseWheel(const Windows::Foundation::Point point, const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const int32_t delta, const ::Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state); + void _ScrollPositionChanged(const IInspectable& sender, const Control::ScrollPositionChangedArgs& args); + void _CursorPositionChanged(const IInspectable& sender, const IInspectable& args); bool _CapturePointer(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); bool _ReleasePointerCapture(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); @@ -263,11 +227,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool _TryHandleKeyBinding(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers) const; void _ClearKeyboardState(const WORD vkey, const WORD scanCode) const noexcept; bool _TrySendKeyEvent(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const bool keyDown); - bool _TrySendMouseEvent(Windows::UI::Input::PointerPoint const& point); - bool _CanSendVTMouseInput(); - const COORD _GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition); - const unsigned int _NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime); + const til::point _toTerminalOrigin(winrt::Windows::Foundation::Point cursorPosition); double _GetAutoScrollSpeed(double cursorDistanceFromBorder) const; void _Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive); @@ -277,11 +238,16 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _CompositionCompleted(winrt::hstring text); void _CurrentCursorPositionHandler(const IInspectable& sender, const CursorPositionEventArgs& eventArgs); void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs); - winrt::fire_and_forget _AsyncCloseConnection(); - winrt::fire_and_forget _RaiseReadOnlyWarning(); + winrt::fire_and_forget _hoveredHyperlinkChanged(IInspectable sender, IInspectable args); - void _UpdateHoveredCell(const std::optional& terminalPosition); + void _coreFontSizeChanged(const int fontWidth, + const int fontHeight, + const bool isInitialChange); + winrt::fire_and_forget _coreTransparencyChanged(IInspectable sender, Control::TransparencyChangedEventArgs args); + void _coreReceivedOutput(const IInspectable& sender, const IInspectable& args); + void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); + void _coreWarningBell(const IInspectable& sender, const IInspectable& args); }; } diff --git a/src/cascadia/TerminalControl/TermControl.idl b/src/cascadia/TerminalControl/TermControl.idl index 560abc8b237..6242436eb01 100644 --- a/src/cascadia/TerminalControl/TermControl.idl +++ b/src/cascadia/TerminalControl/TermControl.idl @@ -5,23 +5,28 @@ import "IMouseWheelListener.idl"; import "IControlSettings.idl"; import "IDirectKeyListener.idl"; import "EventArgs.idl"; +import "ICoreState.idl"; namespace Microsoft.Terminal.Control { - [default_interface] runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl, IDirectKeyListener, IMouseWheelListener + [default_interface] runtimeclass TermControl : Windows.UI.Xaml.Controls.UserControl, + IDirectKeyListener, + IMouseWheelListener, + ICoreState { - TermControl(Microsoft.Terminal.Control.IControlSettings settings, Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); + TermControl(IControlSettings settings, + Microsoft.Terminal.TerminalConnection.ITerminalConnection connection); - static Windows.Foundation.Size GetProposedDimensions(Microsoft.Terminal.Control.IControlSettings settings, UInt32 dpi); + static Windows.Foundation.Size GetProposedDimensions(IControlSettings settings, UInt32 dpi); void UpdateSettings(); Microsoft.Terminal.Control.IControlSettings Settings { get; }; Microsoft.Terminal.Control.IControlAppearance UnfocusedAppearance; - event Windows.Foundation.TypedEventHandler TitleChanged; event FontSizeChangedEventArgs FontSizeChanged; + event Windows.Foundation.TypedEventHandler TitleChanged; event Windows.Foundation.TypedEventHandler CopyToClipboard; event Windows.Foundation.TypedEventHandler PasteFromClipboard; event Windows.Foundation.TypedEventHandler OpenHyperlink; @@ -30,8 +35,6 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler WarningBell; event Windows.Foundation.TypedEventHandler HidePointerCursor; event Windows.Foundation.TypedEventHandler RestorePointerCursor; - - event ScrollPositionChangedEventArgs ScrollPositionChanged; event Windows.Foundation.TypedEventHandler TabColorChanged; event Windows.Foundation.TypedEventHandler ReadOnlyChanged; event Windows.Foundation.TypedEventHandler FocusFollowMouseRequested; @@ -41,11 +44,6 @@ namespace Microsoft.Terminal.Control // We expose this and ConnectionState here so that it might eventually be data bound. event Windows.Foundation.TypedEventHandler ConnectionStateChanged; - Microsoft.Terminal.TerminalConnection.ConnectionState ConnectionState { get; }; - - String Title { get; }; - - Boolean BracketedPasteEnabled { get; }; Boolean CopySelectionToClipboard(Boolean singleLine, Windows.Foundation.IReference formats); void PasteTextFromClipboard(); void Close(); @@ -54,8 +52,6 @@ namespace Microsoft.Terminal.Control Single SnapDimensionToGrid(Boolean widthOrHeight, Single dimension); void ScrollViewport(Int32 viewTop); - Int32 GetScrollOffset(); - Int32 GetViewHeight(); void CreateSearchBoxControl(); @@ -67,14 +63,6 @@ namespace Microsoft.Terminal.Control void ToggleShaderEffects(); void SendInput(String input); - void TaskbarProgressChanged(); - UInt64 TaskbarState { get; }; - UInt64 TaskbarProgress { get; }; - - String WorkingDirectory { get; }; - - Windows.Foundation.IReference TabColor { get; }; - Boolean ReadOnly { get; }; void ToggleReadOnly(); } diff --git a/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp b/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp index f6a629945e6..6df09f3022d 100644 --- a/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp +++ b/src/cascadia/TerminalControl/TermControlAutomationPeer.cpp @@ -199,7 +199,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation #pragma region IControlAccessibilityInfo COORD TermControlAutomationPeer::GetFontSize() const { - return _termControl->GetActualFont().GetSize(); + return _termControl->GetFontSize(); } RECT TermControlAutomationPeer::GetBounds() const diff --git a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj index e2488e2f193..5220de3e4d2 100644 --- a/src/cascadia/TerminalControl/TerminalControlLib.vcxproj +++ b/src/cascadia/TerminalControl/TerminalControlLib.vcxproj @@ -28,12 +28,18 @@ - - EventArgs.idl + + ControlCore.idl + + + ControlInteractivity.idl KeyChord.idl + + EventArgs.idl + SearchBoxControl.xaml @@ -54,6 +60,12 @@ Create + + ControlCore.idl + + + ControlInteractivity.idl + EventArgs.idl @@ -78,9 +90,12 @@ - - + + + + + diff --git a/src/cascadia/TerminalCore/ITerminalInput.hpp b/src/cascadia/TerminalCore/ITerminalInput.hpp index 307f03b1165..c2c7c54ca1b 100644 --- a/src/cascadia/TerminalCore/ITerminalInput.hpp +++ b/src/cascadia/TerminalCore/ITerminalInput.hpp @@ -20,7 +20,6 @@ namespace Microsoft::Terminal::Core virtual bool SendMouseEvent(const COORD viewportPos, const unsigned int uiButton, const ControlKeyStates states, const short wheelDelta, const Microsoft::Console::VirtualTerminal::TerminalInput::MouseButtonState state) = 0; virtual bool SendCharEvent(const wchar_t ch, const WORD scanCode, const ControlKeyStates states) = 0; - // void SendMouseEvent(uint row, uint col, KeyModifiers modifiers); [[nodiscard]] virtual HRESULT UserResize(const COORD size) noexcept = 0; virtual void UserScrollViewport(const int viewTop) = 0; virtual int GetScrollOffset() = 0; diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index ff73855f940..ac3c4820dad 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -1121,7 +1121,7 @@ void Terminal::SetWarningBellCallback(std::function pfn) noexcept _pfnWarningBell.swap(pfn); } -void Terminal::SetTitleChangedCallback(std::function pfn) noexcept +void Terminal::SetTitleChangedCallback(std::function pfn) noexcept { _pfnTitleChanged.swap(pfn); } @@ -1131,7 +1131,7 @@ void Terminal::SetTabColorChangedCallback(std::function pfn) noexcept +void Terminal::SetCopyToClipboardCallback(std::function pfn) noexcept { _pfnCopyToClipboard.swap(pfn); } diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 2efd3ab51d3..9516f73207a 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -196,9 +196,9 @@ class Microsoft::Terminal::Core::Terminal final : void SetWriteInputCallback(std::function pfn) noexcept; void SetWarningBellCallback(std::function pfn) noexcept; - void SetTitleChangedCallback(std::function pfn) noexcept; + void SetTitleChangedCallback(std::function pfn) noexcept; void SetTabColorChangedCallback(std::function)> pfn) noexcept; - void SetCopyToClipboardCallback(std::function pfn) noexcept; + void SetCopyToClipboardCallback(std::function pfn) noexcept; void SetScrollPositionChangedCallback(std::function pfn) noexcept; void SetCursorPositionChangedCallback(std::function pfn) noexcept; void SetBackgroundCallback(std::function pfn) noexcept; @@ -211,6 +211,7 @@ class Microsoft::Terminal::Core::Terminal final : void ClearPatternTree() noexcept; const std::optional GetTabColor() const noexcept; + til::color GetDefaultBackground() const noexcept; Microsoft::Console::Render::BlinkingState& GetBlinkingState() const noexcept; @@ -230,14 +231,14 @@ class Microsoft::Terminal::Core::Terminal final : void SetSelectionEnd(const COORD position, std::optional newExpansionMode = std::nullopt); void SetBlockSelection(const bool isEnabled) noexcept; - const TextBuffer::TextAndColor RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace) const; + const TextBuffer::TextAndColor RetrieveSelectedTextFromBuffer(bool trimTrailingWhitespace); #pragma endregion private: std::function _pfnWriteInput; std::function _pfnWarningBell; - std::function _pfnTitleChanged; - std::function _pfnCopyToClipboard; + std::function _pfnTitleChanged; + std::function _pfnCopyToClipboard; std::function _pfnScrollPositionChanged; std::function _pfnBackgroundColorChanged; std::function _pfnCursorPositionChanged; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 891e251ff68..f58a143a10b 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -477,6 +477,11 @@ try } CATCH_LOG_RETURN_FALSE() +til::color Terminal::GetDefaultBackground() const noexcept +{ + return _defaultBg; +} + bool Terminal::EnableWin32InputMode(const bool win32InputMode) noexcept { _terminalInput->ChangeWin32InputMode(win32InputMode); diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp index af8167ea016..c8c877032a7 100644 --- a/src/cascadia/TerminalCore/TerminalSelection.cpp +++ b/src/cascadia/TerminalCore/TerminalSelection.cpp @@ -249,8 +249,10 @@ void Terminal::ClearSelection() // - singleLine: collapse all of the text to one line // Return Value: // - wstring text from buffer. If extended to multiple lines, each line is separated by \r\n -const TextBuffer::TextAndColor Terminal::RetrieveSelectedTextFromBuffer(bool singleLine) const +const TextBuffer::TextAndColor Terminal::RetrieveSelectedTextFromBuffer(bool singleLine) { + auto lock = LockForReading(); + const auto selectionRects = _GetSelectionRects(); const auto GetAttributeColors = std::bind(&Terminal::GetAttributeColors, this, std::placeholders::_1); diff --git a/src/cascadia/UnitTests_Control/Control.UnitTests.vcxproj b/src/cascadia/UnitTests_Control/Control.UnitTests.vcxproj index 8c85d1b6ad8..5ebe26096cb 100644 --- a/src/cascadia/UnitTests_Control/Control.UnitTests.vcxproj +++ b/src/cascadia/UnitTests_Control/Control.UnitTests.vcxproj @@ -5,7 +5,7 @@ Win32Proj ControlUnitTests UnitTests_Control - Control.UnitTests + Control.Unit.Tests DynamicLibrary 10.0.18362.0 10.0.18362.0 @@ -18,11 +18,14 @@ + + + Create @@ -42,11 +45,16 @@ - - + + + $(OpenConsoleCommonOutDir)TerminalConnection\Microsoft.Terminal.TerminalConnection.winmd + true + false + false + + @@ -76,7 +84,7 @@ - + diff --git a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp index 515b2f30d06..96617b513ae 100644 --- a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp @@ -3,6 +3,9 @@ #include "pch.h" #include "../TerminalControl/EventArgs.h" +#include "../TerminalControl/ControlCore.h" +#include "MockControlSettings.h" +#include "MockConnection.h" using namespace Microsoft::Console; using namespace WEX::Logging; @@ -17,15 +20,181 @@ namespace ControlUnitTests class ControlCoreTests { BEGIN_TEST_CLASS(ControlCoreTests) + TEST_CLASS_PROPERTY(L"TestTimeout", L"0:0:10") // 10s timeout END_TEST_CLASS() - TEST_METHOD(PlaceholderTest); + TEST_METHOD(ComPtrSettings); + TEST_METHOD(InstantiateCore); + TEST_METHOD(TestInitialize); + TEST_METHOD(TestAdjustAcrylic); + + TEST_METHOD(TestFreeAfterClose); + + TEST_METHOD(TestFontInitializedInCtor); + + TEST_CLASS_SETUP(ModuleSetup) + { + winrt::init_apartment(winrt::apartment_type::single_threaded); + + return true; + } + TEST_CLASS_CLEANUP(ClassCleanup) + { + winrt::uninit_apartment(); + return true; + } + + std::tuple, winrt::com_ptr> _createSettingsAndConnection() + { + Log::Comment(L"Create settings object"); + auto settings = winrt::make_self(); + VERIFY_IS_NOT_NULL(settings); + + Log::Comment(L"Create connection object"); + auto conn = winrt::make_self(); + VERIFY_IS_NOT_NULL(conn); + + return { settings, conn }; + } }; - void ControlCoreTests::PlaceholderTest() + void ControlCoreTests::ComPtrSettings() + { + Log::Comment(L"Just make sure we can instantiate a settings obj in a com_ptr"); + auto settings = winrt::make_self(); + + Log::Comment(L"Verify literally any setting, it doesn't matter"); + VERIFY_ARE_EQUAL(DEFAULT_FOREGROUND, settings->DefaultForeground()); + } + + void ControlCoreTests::InstantiateCore() { - Log::Comment(L"This test is a placeholder while the rest of this test library is being authored."); - VERIFY_IS_TRUE(true); + auto [settings, conn] = _createSettingsAndConnection(); + + Log::Comment(L"Create ControlCore object"); + auto core = winrt::make_self(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + } + + void ControlCoreTests::TestInitialize() + { + auto [settings, conn] = _createSettingsAndConnection(); + + Log::Comment(L"Create ControlCore object"); + auto core = winrt::make_self(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + + VERIFY_IS_FALSE(core->_initializedTerminal); + // "Consolas" ends up with an actual size of 9x21 at 96DPI. So + // let's just arbitrarily start with a 270x420px (30x20 chars) window + core->Initialize(270, 420, 1.0); + VERIFY_IS_TRUE(core->_initializedTerminal); + VERIFY_ARE_EQUAL(30, core->_terminal->GetViewport().Width()); + } + + void ControlCoreTests::TestAdjustAcrylic() + { + auto [settings, conn] = _createSettingsAndConnection(); + + settings->UseAcrylic(true); + settings->TintOpacity(0.5f); + + Log::Comment(L"Create ControlCore object"); + auto core = winrt::make_self(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + + // A callback to make sure that we're raising TransparencyChanged events + double expectedOpacity = 0.5; + auto opacityCallback = [&](auto&&, Control::TransparencyChangedEventArgs args) mutable { + VERIFY_ARE_EQUAL(expectedOpacity, args.Opacity()); + VERIFY_ARE_EQUAL(expectedOpacity, settings->TintOpacity()); + VERIFY_ARE_EQUAL(expectedOpacity, core->_settings.TintOpacity()); + + if (expectedOpacity < 1.0) + { + VERIFY_IS_TRUE(settings->UseAcrylic()); + VERIFY_IS_TRUE(core->_settings.UseAcrylic()); + } + VERIFY_ARE_EQUAL(expectedOpacity < 1.0, settings->UseAcrylic()); + VERIFY_ARE_EQUAL(expectedOpacity < 1.0, core->_settings.UseAcrylic()); + }; + core->TransparencyChanged(opacityCallback); + + VERIFY_IS_FALSE(core->_initializedTerminal); + // "Cascadia Mono" ends up with an actual size of 9x19 at 96DPI. So + // let's just arbitrarily start with a 270x380px (30x20 chars) window + core->Initialize(270, 380, 1.0); + VERIFY_IS_TRUE(core->_initializedTerminal); + + Log::Comment(L"Increasing opacity till fully opaque"); + expectedOpacity += 0.1; // = 0.6; + core->AdjustOpacity(0.1); + expectedOpacity += 0.1; // = 0.7; + core->AdjustOpacity(0.1); + expectedOpacity += 0.1; // = 0.8; + core->AdjustOpacity(0.1); + expectedOpacity += 0.1; // = 0.9; + core->AdjustOpacity(0.1); + expectedOpacity += 0.1; // = 1.0; + // cast to float because floating point numbers are mean + VERIFY_ARE_EQUAL(1.0f, base::saturated_cast(expectedOpacity)); + core->AdjustOpacity(0.1); + + Log::Comment(L"Increasing opacity more doesn't actually change it to be >1.0"); + + expectedOpacity = 1.0; + core->AdjustOpacity(0.1); + + Log::Comment(L"Decrease opacity"); + expectedOpacity -= 0.25; // = 0.75; + core->AdjustOpacity(-0.25); + expectedOpacity -= 0.25; // = 0.5; + core->AdjustOpacity(-0.25); + expectedOpacity -= 0.25; // = 0.25; + core->AdjustOpacity(-0.25); + expectedOpacity -= 0.25; // = 0.05; + // cast to float because floating point numbers are mean + VERIFY_ARE_EQUAL(0.0f, base::saturated_cast(expectedOpacity)); + core->AdjustOpacity(-0.25); + + Log::Comment(L"Decreasing opacity more doesn't actually change it to be < 0"); + expectedOpacity = 0.0; + core->AdjustOpacity(-0.25); + } + + void ControlCoreTests::TestFreeAfterClose() + { + { + auto [settings, conn] = _createSettingsAndConnection(); + + Log::Comment(L"Create ControlCore object"); + auto core = winrt::make_self(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + + Log::Comment(L"Close the Core, like a TermControl would"); + core->Close(); + } + + VERIFY_IS_TRUE(true, L"Make sure that the test didn't crash when the core when out of scope"); + } + + void ControlCoreTests::TestFontInitializedInCtor() + { + // This is to catch a dumb programming mistake I made while working on + // the core/control split. We want the font initialized in the ctor, + // before we even get to Core::Initialize. + + auto [settings, conn] = _createSettingsAndConnection(); + + // Make sure to use something dumb like "Impact" as a font name here so + // that you don't default to Cascadia* + settings->FontFace(L"Impact"); + + Log::Comment(L"Create ControlCore object"); + auto core = winrt::make_self(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + + VERIFY_ARE_EQUAL(L"Impact", std::wstring_view{ core->_actualFont.GetFaceName() }); } } diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp new file mode 100644 index 00000000000..eeb9825475f --- /dev/null +++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp @@ -0,0 +1,333 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "pch.h" +#include "../TerminalControl/EventArgs.h" +#include "../TerminalControl/ControlInteractivity.h" +#include "MockControlSettings.h" +#include "MockConnection.h" + +using namespace ::Microsoft::Console; +using namespace WEX::Logging; +using namespace WEX::TestExecution; +using namespace WEX::Common; + +using namespace winrt; +using namespace winrt::Microsoft::Terminal; +using namespace ::Microsoft::Terminal::Core; +using namespace ::Microsoft::Console::VirtualTerminal; + +namespace ControlUnitTests +{ + class ControlInteractivityTests + { + BEGIN_TEST_CLASS(ControlInteractivityTests) + TEST_CLASS_PROPERTY(L"TestTimeout", L"0:0:10") // 10s timeout + END_TEST_CLASS() + + TEST_METHOD(TestAdjustAcrylic); + TEST_METHOD(TestScrollWithMouse); + + TEST_METHOD(CreateSubsequentSelectionWithDragging); + + TEST_CLASS_SETUP(ClassSetup) + { + winrt::init_apartment(winrt::apartment_type::single_threaded); + + return true; + } + TEST_CLASS_CLEANUP(ClassCleanup) + { + winrt::uninit_apartment(); + return true; + } + + std::tuple, + winrt::com_ptr> + _createSettingsAndConnection() + { + Log::Comment(L"Create settings object"); + auto settings = winrt::make_self(); + VERIFY_IS_NOT_NULL(settings); + + Log::Comment(L"Create connection object"); + auto conn = winrt::make_self(); + VERIFY_IS_NOT_NULL(conn); + + return { settings, conn }; + } + + std::tuple, + winrt::com_ptr> + _createCoreAndInteractivity(Control::IControlSettings settings, + TerminalConnection::ITerminalConnection conn) + { + Log::Comment(L"Create ControlInteractivity object"); + auto interactivity = winrt::make_self(settings, conn); + VERIFY_IS_NOT_NULL(interactivity); + auto core = interactivity->_core; + VERIFY_IS_NOT_NULL(core); + + return { core, interactivity }; + } + + void _standardInit(winrt::com_ptr core, + winrt::com_ptr interactivity) + { + // "Consolas" ends up with an actual size of 9x21 at 96DPI. So + // let's just arbitrarily start with a 270x420px (30x20 chars) window + core->Initialize(270, 420, 1.0); + VERIFY_IS_TRUE(core->_initializedTerminal); + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + interactivity->Initialize(); + } + }; + + void ControlInteractivityTests::TestAdjustAcrylic() + { + Log::Comment(L"Test that scrolling the mouse wheel with Ctrl+Shift changes opacity"); + Log::Comment(L"(This test won't log as it goes, because it does some 200 verifications.)"); + + WEX::TestExecution::SetVerifyOutput verifyOutputScope{ WEX::TestExecution::VerifyOutputSettings::LogOnlyFailures }; + + auto [settings, conn] = _createSettingsAndConnection(); + + settings->UseAcrylic(true); + settings->TintOpacity(0.5f); + + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + + // A callback to make sure that we're raising TransparencyChanged events + double expectedOpacity = 0.5; + auto opacityCallback = [&](auto&&, Control::TransparencyChangedEventArgs args) mutable { + VERIFY_ARE_EQUAL(expectedOpacity, args.Opacity()); + VERIFY_ARE_EQUAL(expectedOpacity, settings->TintOpacity()); + VERIFY_ARE_EQUAL(expectedOpacity, core->_settings.TintOpacity()); + + if (expectedOpacity < 1.0) + { + VERIFY_IS_TRUE(settings->UseAcrylic()); + VERIFY_IS_TRUE(core->_settings.UseAcrylic()); + } + VERIFY_ARE_EQUAL(expectedOpacity < 1.0, settings->UseAcrylic()); + VERIFY_ARE_EQUAL(expectedOpacity < 1.0, core->_settings.UseAcrylic()); + }; + core->TransparencyChanged(opacityCallback); + + const auto modifiers = ControlKeyStates(ControlKeyStates::RightCtrlPressed | ControlKeyStates::ShiftPressed); + + Log::Comment(L"Scroll in the positive direction, increasing opacity"); + // Scroll more than enough times to get to 1.0 from .5. + for (int i = 0; i < 55; i++) + { + // each mouse wheel only adjusts opacity by .01 + expectedOpacity += 0.01; + if (expectedOpacity >= 1.0) + { + expectedOpacity = 1.0; + } + + // The mouse location and buttons don't matter here. + interactivity->MouseWheel(modifiers, + 30, + til::point{ 0, 0 }, + { false, false, false }); + } + + Log::Comment(L"Scroll in the negative direction, decreasing opacity"); + // Scroll more than enough times to get to 0.0 from 1.0 + for (int i = 0; i < 105; i++) + { + // each mouse wheel only adjusts opacity by .01 + expectedOpacity -= 0.01; + if (expectedOpacity <= 0.0) + { + expectedOpacity = 0.0; + } + + // The mouse location and buttons don't matter here. + interactivity->MouseWheel(modifiers, + 30, + til::point{ 0, 0 }, + { false, false, false }); + } + } + + void ControlInteractivityTests::TestScrollWithMouse() + { + WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{}; + + auto [settings, conn] = _createSettingsAndConnection(); + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + _standardInit(core, interactivity); + // For the sake of this test, scroll one line at a time + interactivity->_rowsToScroll = 1; + + int expectedTop = 0; + int expectedViewHeight = 20; + int expectedBufferHeight = 20; + + auto scrollChangedHandler = [&](auto&&, const Control::ScrollPositionChangedArgs& args) mutable { + VERIFY_ARE_EQUAL(expectedTop, args.ViewTop()); + VERIFY_ARE_EQUAL(expectedViewHeight, args.ViewHeight()); + VERIFY_ARE_EQUAL(expectedBufferHeight, args.BufferSize()); + }; + core->ScrollPositionChanged(scrollChangedHandler); + interactivity->ScrollPositionChanged(scrollChangedHandler); + + for (int i = 0; i < 40; ++i) + { + Log::Comment(NoThrowString().Format(L"Writing line #%d", i)); + // The \r\n in the 19th loop will cause the view to start moving + if (i >= 19) + { + expectedTop++; + expectedBufferHeight++; + } + + conn->WriteInput(L"Foo\r\n"); + } + // We printed that 40 times, but the final \r\n bumped the view down one MORE row. + VERIFY_ARE_EQUAL(20, core->_terminal->GetViewport().Height()); + VERIFY_ARE_EQUAL(21, core->ScrollOffset()); + VERIFY_ARE_EQUAL(20, core->ViewHeight()); + VERIFY_ARE_EQUAL(41, core->BufferHeight()); + + Log::Comment(L"Scroll up a line"); + const auto modifiers = ControlKeyStates(); + expectedBufferHeight = 41; + expectedTop = 20; + + interactivity->MouseWheel(modifiers, + WHEEL_DELTA, + til::point{ 0, 0 }, + { false, false, false }); + + Log::Comment(L"Scroll up 19 more times, to the top"); + for (int i = 0; i < 20; ++i) + { + expectedTop--; + interactivity->MouseWheel(modifiers, + WHEEL_DELTA, + til::point{ 0, 0 }, + { false, false, false }); + } + Log::Comment(L"Scrolling up more should do nothing"); + expectedTop = 0; + interactivity->MouseWheel(modifiers, + WHEEL_DELTA, + til::point{ 0, 0 }, + { false, false, false }); + interactivity->MouseWheel(modifiers, + WHEEL_DELTA, + til::point{ 0, 0 }, + { false, false, false }); + + Log::Comment(L"Scroll down 21 more times, to the bottom"); + for (int i = 0; i < 21; ++i) + { + expectedTop++; + interactivity->MouseWheel(modifiers, + -WHEEL_DELTA, + til::point{ 0, 0 }, + { false, false, false }); + } + Log::Comment(L"Scrolling up more should do nothing"); + expectedTop = 21; + interactivity->MouseWheel(modifiers, + -WHEEL_DELTA, + til::point{ 0, 0 }, + { false, false, false }); + interactivity->MouseWheel(modifiers, + -WHEEL_DELTA, + til::point{ 0, 0 }, + { false, false, false }); + } + + void ControlInteractivityTests::CreateSubsequentSelectionWithDragging() + { + // This is a test for GH#9725 + WEX::TestExecution::DisableVerifyExceptions disableVerifyExceptions{}; + + auto [settings, conn] = _createSettingsAndConnection(); + auto [core, interactivity] = _createCoreAndInteractivity(*settings, *conn); + _standardInit(core, interactivity); + + // For this test, don't use any modifiers + const auto modifiers = ControlKeyStates(); + const TerminalInput::MouseButtonState leftMouseDown{ true, false, false }; + const TerminalInput::MouseButtonState noMouseDown{ false, false, false }; + + const til::size fontSize{ 9, 21 }; + + Log::Comment(L"Click on the terminal"); + const til::point terminalPosition0{ 0, 0 }; + const til::point cursorPosition0 = terminalPosition0 * fontSize; + interactivity->PointerPressed(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + 0, // timestamp + modifiers, + cursorPosition0); + Log::Comment(L"Verify that there's not yet a selection"); + + VERIFY_IS_FALSE(core->HasSelection()); + + Log::Comment(L"Drag the mouse just a little"); + // move not quite a whole cell, but enough to start a selection + const til::point terminalPosition1{ 0, 0 }; + const til::point cursorPosition1{ 6, 0 }; + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition1); + Log::Comment(L"Verify that there's one selection"); + VERIFY_IS_TRUE(core->HasSelection()); + VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size()); + + Log::Comment(L"Drag the mouse down a whole row"); + const til::point terminalPosition2{ 1, 1 }; + const til::point cursorPosition2 = terminalPosition2 * fontSize; + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition2); + Log::Comment(L"Verify that there's now two selections (one on each row)"); + VERIFY_IS_TRUE(core->HasSelection()); + VERIFY_ARE_EQUAL(2u, core->_terminal->GetSelectionRects().size()); + + Log::Comment(L"Release the mouse"); + interactivity->PointerReleased(noMouseDown, + WM_LBUTTONUP, //pointerUpdateKind + modifiers, + cursorPosition2); + Log::Comment(L"Verify that there's still two selections"); + VERIFY_IS_TRUE(core->HasSelection()); + VERIFY_ARE_EQUAL(2u, core->_terminal->GetSelectionRects().size()); + + Log::Comment(L"click outside the current selection"); + const til::point terminalPosition3{ 2, 2 }; + const til::point cursorPosition3 = terminalPosition3 * fontSize; + interactivity->PointerPressed(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + 0, // timestamp + modifiers, + cursorPosition3); + Log::Comment(L"Verify that there's now no selection"); + VERIFY_IS_FALSE(core->HasSelection()); + VERIFY_ARE_EQUAL(0u, core->_terminal->GetSelectionRects().size()); + + Log::Comment(L"Drag the mouse"); + const til::point terminalPosition4{ 3, 2 }; + const til::point cursorPosition4 = terminalPosition4 * fontSize; + interactivity->PointerMoved(leftMouseDown, + WM_LBUTTONDOWN, //pointerUpdateKind + modifiers, + true, // focused, + cursorPosition4); + Log::Comment(L"Verify that there's now one selection"); + VERIFY_IS_TRUE(core->HasSelection()); + VERIFY_ARE_EQUAL(1u, core->_terminal->GetSelectionRects().size()); + } +} diff --git a/src/cascadia/UnitTests_Control/MockConnection.h b/src/cascadia/UnitTests_Control/MockConnection.h new file mode 100644 index 00000000000..14b024df108 --- /dev/null +++ b/src/cascadia/UnitTests_Control/MockConnection.h @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +// +// This is literally just the EchoConnection, but we can't use the +// EchoConnection because it's in TerminalConnection.dll and loading that in +// these tests is fraught with peril. Easier just to have a local copy. + +#pragma once + +#include "../cascadia/inc/cppwinrt_utils.h" + +namespace ControlUnitTests +{ + class MockConnection : public winrt::implements + { + public: + MockConnection() noexcept = default; + + void Start() noexcept {}; + void WriteInput(winrt::hstring const& data) + { + _TerminalOutputHandlers(data); + } + void Resize(uint32_t /*rows*/, uint32_t /*columns*/) noexcept {} + void Close() noexcept {} + + winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept { return winrt::Microsoft::Terminal::TerminalConnection::ConnectionState::Connected; } + + WINRT_CALLBACK(TerminalOutput, winrt::Microsoft::Terminal::TerminalConnection::TerminalOutputHandler); + TYPED_EVENT(StateChanged, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection, IInspectable); + }; +} diff --git a/src/cascadia/UnitTests_Control/MockControlSettings.h b/src/cascadia/UnitTests_Control/MockControlSettings.h new file mode 100644 index 00000000000..06392d440d4 --- /dev/null +++ b/src/cascadia/UnitTests_Control/MockControlSettings.h @@ -0,0 +1,90 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. +--*/ +#pragma once + +#include "../inc/cppwinrt_utils.h" +#include +#include + +namespace ControlUnitTests +{ + class MockControlSettings : public winrt::implements + { + public: + MockControlSettings() = default; + + // --------------------------- Core Settings --------------------------- + // All of these settings are defined in ICoreSettings. + + WINRT_PROPERTY(til::color, DefaultForeground, DEFAULT_FOREGROUND); + WINRT_PROPERTY(til::color, DefaultBackground, DEFAULT_BACKGROUND); + WINRT_PROPERTY(til::color, SelectionBackground, DEFAULT_FOREGROUND); + WINRT_PROPERTY(int32_t, HistorySize, DEFAULT_HISTORY_SIZE); + WINRT_PROPERTY(int32_t, InitialRows, 30); + WINRT_PROPERTY(int32_t, InitialCols, 80); + + WINRT_PROPERTY(bool, SnapOnInput, true); + WINRT_PROPERTY(bool, AltGrAliasing, true); + WINRT_PROPERTY(til::color, CursorColor, DEFAULT_CURSOR_COLOR); + WINRT_PROPERTY(winrt::Microsoft::Terminal::Core::CursorStyle, CursorShape, winrt::Microsoft::Terminal::Core::CursorStyle::Vintage); + WINRT_PROPERTY(uint32_t, CursorHeight, DEFAULT_CURSOR_HEIGHT); + WINRT_PROPERTY(winrt::hstring, WordDelimiters, DEFAULT_WORD_DELIMITERS); + WINRT_PROPERTY(bool, CopyOnSelect, false); + WINRT_PROPERTY(bool, InputServiceWarning, true); + WINRT_PROPERTY(bool, FocusFollowMouse, false); + + WINRT_PROPERTY(winrt::Windows::Foundation::IReference, TabColor, nullptr); + + WINRT_PROPERTY(winrt::Windows::Foundation::IReference, StartingTabColor, nullptr); + + winrt::Microsoft::Terminal::Core::ICoreAppearance UnfocusedAppearance() { return {}; }; + + WINRT_PROPERTY(bool, TrimBlockSelection, false); + // ------------------------ End of Core Settings ----------------------- + + WINRT_PROPERTY(winrt::hstring, ProfileName); + WINRT_PROPERTY(bool, UseAcrylic, false); + WINRT_PROPERTY(double, TintOpacity, 0.5); + WINRT_PROPERTY(winrt::hstring, Padding, DEFAULT_PADDING); + WINRT_PROPERTY(winrt::hstring, FontFace, L"Consolas"); + WINRT_PROPERTY(int32_t, FontSize, DEFAULT_FONT_SIZE); + + WINRT_PROPERTY(winrt::Windows::UI::Text::FontWeight, FontWeight); + + WINRT_PROPERTY(winrt::hstring, BackgroundImage); + WINRT_PROPERTY(double, BackgroundImageOpacity, 1.0); + + WINRT_PROPERTY(winrt::Windows::UI::Xaml::Media::Stretch, BackgroundImageStretchMode, winrt::Windows::UI::Xaml::Media::Stretch::UniformToFill); + WINRT_PROPERTY(winrt::Windows::UI::Xaml::HorizontalAlignment, BackgroundImageHorizontalAlignment, winrt::Windows::UI::Xaml::HorizontalAlignment::Center); + WINRT_PROPERTY(winrt::Windows::UI::Xaml::VerticalAlignment, BackgroundImageVerticalAlignment, winrt::Windows::UI::Xaml::VerticalAlignment::Center); + + WINRT_PROPERTY(winrt::Microsoft::Terminal::Control::IKeyBindings, KeyBindings, nullptr); + + WINRT_PROPERTY(winrt::hstring, Commandline); + WINRT_PROPERTY(winrt::hstring, StartingDirectory); + WINRT_PROPERTY(winrt::hstring, StartingTitle); + WINRT_PROPERTY(bool, SuppressApplicationTitle); + WINRT_PROPERTY(winrt::hstring, EnvironmentVariables); + + WINRT_PROPERTY(winrt::Microsoft::Terminal::Control::ScrollbarState, ScrollState, winrt::Microsoft::Terminal::Control::ScrollbarState::Visible); + + WINRT_PROPERTY(winrt::Microsoft::Terminal::Control::TextAntialiasingMode, AntialiasingMode, winrt::Microsoft::Terminal::Control::TextAntialiasingMode::Grayscale); + + WINRT_PROPERTY(bool, RetroTerminalEffect, false); + WINRT_PROPERTY(bool, ForceFullRepaintRendering, false); + WINRT_PROPERTY(bool, SoftwareRendering, false); + WINRT_PROPERTY(bool, ForceVTInput, false); + + WINRT_PROPERTY(winrt::hstring, PixelShaderPath); + + private: + std::array _ColorTable; + + public: + winrt::Microsoft::Terminal::Core::Color GetColorTableEntry(int32_t index) noexcept { return _ColorTable.at(index); } + std::array ColorTable() { return _ColorTable; } + void ColorTable(std::array /*colors*/) {} + }; +} diff --git a/src/cascadia/UnitTests_Control/pch.h b/src/cascadia/UnitTests_Control/pch.h index 8239e461c15..7568822f28f 100644 --- a/src/cascadia/UnitTests_Control/pch.h +++ b/src/cascadia/UnitTests_Control/pch.h @@ -40,6 +40,10 @@ Licensed under the MIT license. #include #include +#include +#include +#include + // Manually include til after we include Windows.Foundation to give it winrt superpowers #include "til.h" diff --git a/src/cascadia/UnitTests_Remoting/Remoting.UnitTests.vcxproj b/src/cascadia/UnitTests_Remoting/Remoting.UnitTests.vcxproj index a151a594027..a67eba4f3d2 100644 --- a/src/cascadia/UnitTests_Remoting/Remoting.UnitTests.vcxproj +++ b/src/cascadia/UnitTests_Remoting/Remoting.UnitTests.vcxproj @@ -16,7 +16,7 @@ Win32Proj RemotingUnitTests UnitTests_Remoting - Remoting.UnitTests + Remoting.Unit.Tests DynamicLibrary 10.0.18362.0 10.0.18362.0 diff --git a/src/renderer/dx/DxRenderer.cpp b/src/renderer/dx/DxRenderer.cpp index 0ffbc1584ec..1763f3c5b5a 100644 --- a/src/renderer/dx/DxRenderer.cpp +++ b/src/renderer/dx/DxRenderer.cpp @@ -2166,6 +2166,7 @@ try if (_antialiasingMode != antialiasingMode) { _antialiasingMode = antialiasingMode; + _recreateDeviceRequested = true; LOG_IF_FAILED(InvalidateAll()); } } diff --git a/src/renderer/dx/DxRenderer.hpp b/src/renderer/dx/DxRenderer.hpp index 39cbfbf8969..7de6832146e 100644 --- a/src/renderer/dx/DxRenderer.hpp +++ b/src/renderer/dx/DxRenderer.hpp @@ -128,6 +128,8 @@ namespace Microsoft::Console::Render void SetAntialiasingMode(const D2D1_TEXT_ANTIALIAS_MODE antialiasingMode) noexcept; void SetDefaultTextBackgroundOpacity(const float opacity) noexcept; + wil::unique_handle _swapChainHandle; + void UpdateHyperlinkHoveredId(const uint16_t hoveredId) noexcept; protected: diff --git a/tools/runut.cmd b/tools/runut.cmd index 3da64494fc3..48cb13a39e6 100644 --- a/tools/runut.cmd +++ b/tools/runut.cmd @@ -23,8 +23,8 @@ call %TAEF% ^ %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\Types.Unit.Tests.dll ^ %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\til.unit.tests.dll ^ %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\UnitTests_TerminalApp\Terminal.App.Unit.Tests.dll ^ - %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\UnitTests_Remoting\Remoting.UnitTests.dll ^ - %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\UnitTests_Control\Control.UnitTests.dll ^ + %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\UnitTests_Remoting\Remoting.Unit.Tests.dll ^ + %OPENCON%\bin\%PLATFORM%\%_LAST_BUILD_CONF%\UnitTests_Control\Control.Unit.Tests.dll ^ %_TestHostAppPath%\TerminalApp.LocalTests.dll ^ %_TestHostAppPath%\SettingsModel.LocalTests.dll ^ %* diff --git a/tools/tests.xml b/tools/tests.xml index 17c51ceecc3..19507741d04 100644 --- a/tools/tests.xml +++ b/tools/tests.xml @@ -6,8 +6,8 @@ - - + +