From 8910a16fd0d0ebf0feaa7b182a5f812a0b28f2d4 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 27 Apr 2021 10:50:45 -0500 Subject: [PATCH] Split `TermControl` into a Core, Interactivity, and Control layer (#9820) ## Summary of the Pull Request Brace yourselves, it's finally here. This PR does the dirty work of splitting the monolithic `TermControl` into three components. These components are: * `ControlCore`: This encapsulates the `Terminal` instance, the `DxEngine` and `Renderer`, and the `Connection`. This is intended to everything that someone might need to stand up a terminal instance in a control, but without any regard for how the UX works. * `ControlInteractivity`: This is a wrapper for the `ControlCore`, which 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. * `TermControl`: This is the UWP control. It's got a Core and Interactivity inside it, which it uses for the actual logic of the terminal itself. TermControl's main responsibility is now By splitting into smaller pieces, it will enable us to * write unit tests for the `Core` and `Interactivity` bits, which we desparately need * Combine `ControlCore` and `ControlInteractivity` in an out-of-proc core process in the future, to enable tab tearout. However, we're not doing that work quite yet. There's still lots of work to be done to enable that, thought this is likely the biggest portion. Ideally, this would just be methods moved wholesale from one file to another. Unfortunately, there are a bunch of cases where that didn't work as well as expected. Especially when trying to better enforce the boundary between the classes. We've got a couple tests here that I've added. These are partially examples, and partially things I ran into while implementing this. A bunch of things from #7001 can go in now that we have this. This PR is gonna be a huge pain to review - 38 files with 3,730 additions and 1,661 deletions is nothing to scoff at. It will also conflict 100% with anything that's targeting `TermControl`. I'm hoping we can review this over the course of the next week and just be done with it, and leave plenty of runway for 1.9 bugs in post. ## References * In pursuit of #1256 * Proc Model: #5000 * https://github.com/microsoft/terminal/projects/5 ## PR Checklist * [x] Closes #6842 * [x] Closes https://github.com/microsoft/terminal/projects/5#card-50760249 * [x] Closes https://github.com/microsoft/terminal/projects/5#card-50760258 * [x] I work here * [x] Tests added/passed * [n/a] Requires documentation to be updated ## Detailed Description of the Pull Request / Additional comments * I don't love the names `ControlCore` and `ControlInteractivity`. Open to other names. * I added a `ICoreState` interface for "properties that come from the `ControlCore`, but consumers of the `TermControl` need to know". In the future, these will all need to be handled specially, because they might involve an RPC call to retrieve the info from the core (or cache it) in the window process. * I've added more `EventArgs` to make more events proper `TypedEvent`s. * I've changed how the TerminalApp layer requests updated TaskbarProgress state. It doesn't need to pump TermControl to raise a new event anymore. * ~~Something that snuck into this branch in the very long history is the switch to `DCompositionCreateSurfaceHandle` for the `DxEngine`. @miniksa wrote this originally in 30b8335, I'm just finally committing it here. We'll need that in the future for the out-of-proc stuff.~~ * I reverted this in c113b65d9. We can revert _that_ commit when we want to come back to it. * I've changed the acrylic handler a decent amount. But added tests! * All the `ThrottledFunc` things are left in `TermControl`. Some might be able to move down into core/interactivity, but once we figure out how to use a different kind of Dispatcher (because a UI thread won't necessarily exist for those components). * I've undoubtably messed up the merging of the locking around the appearance config stuff recently ## Validation Steps Performed I've got a rolling list in https://github.com/microsoft/terminal/issues/6842#issuecomment-810990460 that I'm updating as I go. --- .github/actions/spelling/dictionary/apis.txt | 4 + .vscode/settings.json | 15 +- .vscode/tasks.json | 7 +- OpenConsole.sln | 2 + .../templates/build-console-steps.yml | 6 +- src/cascadia/TerminalApp/Pane.h | 3 +- src/cascadia/TerminalApp/SettingsTab.h | 2 - src/cascadia/TerminalApp/TabManagement.cpp | 5 + src/cascadia/TerminalApp/TerminalPage.cpp | 63 +- src/cascadia/TerminalApp/TerminalPage.h | 4 +- src/cascadia/TerminalApp/TerminalTab.cpp | 94 +- src/cascadia/TerminalApp/TerminalTab.h | 3 + src/cascadia/TerminalControl/ControlCore.cpp | 1372 ++++++++++++ src/cascadia/TerminalControl/ControlCore.h | 240 ++ src/cascadia/TerminalControl/ControlCore.idl | 16 + .../TerminalControl/ControlInteractivity.cpp | 531 +++++ .../TerminalControl/ControlInteractivity.h | 142 ++ .../TerminalControl/ControlInteractivity.idl | 17 + src/cascadia/TerminalControl/EventArgs.cpp | 1 + src/cascadia/TerminalControl/EventArgs.h | 39 +- src/cascadia/TerminalControl/EventArgs.idl | 5 + src/cascadia/TerminalControl/ICoreState.idl | 25 + src/cascadia/TerminalControl/TermControl.cpp | 1926 ++++------------- src/cascadia/TerminalControl/TermControl.h | 182 +- src/cascadia/TerminalControl/TermControl.idl | 30 +- .../TermControlAutomationPeer.cpp | 2 +- .../TerminalControlLib.vcxproj | 23 +- src/cascadia/TerminalCore/ITerminalInput.hpp | 1 - src/cascadia/TerminalCore/Terminal.cpp | 4 +- src/cascadia/TerminalCore/Terminal.hpp | 11 +- src/cascadia/TerminalCore/TerminalApi.cpp | 5 + .../TerminalCore/TerminalSelection.cpp | 4 +- .../Control.UnitTests.vcxproj | 18 +- .../UnitTests_Control/ControlCoreTests.cpp | 177 +- .../ControlInteractivityTests.cpp | 333 +++ .../UnitTests_Control/MockConnection.h | 32 + .../UnitTests_Control/MockControlSettings.h | 90 + src/cascadia/UnitTests_Control/pch.h | 4 + .../Remoting.UnitTests.vcxproj | 2 +- src/renderer/dx/DxRenderer.cpp | 1 + src/renderer/dx/DxRenderer.hpp | 2 + tools/runut.cmd | 4 +- tools/tests.xml | 4 +- 43 files changed, 3740 insertions(+), 1711 deletions(-) create mode 100644 src/cascadia/TerminalControl/ControlCore.cpp create mode 100644 src/cascadia/TerminalControl/ControlCore.h create mode 100644 src/cascadia/TerminalControl/ControlCore.idl create mode 100644 src/cascadia/TerminalControl/ControlInteractivity.cpp create mode 100644 src/cascadia/TerminalControl/ControlInteractivity.h create mode 100644 src/cascadia/TerminalControl/ControlInteractivity.idl create mode 100644 src/cascadia/TerminalControl/ICoreState.idl create mode 100644 src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp create mode 100644 src/cascadia/UnitTests_Control/MockConnection.h create mode 100644 src/cascadia/UnitTests_Control/MockControlSettings.h diff --git a/.github/actions/spelling/dictionary/apis.txt b/.github/actions/spelling/dictionary/apis.txt index a6087aa927..6d75da3027 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 4534d56015..1b6028e5ae 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 e9942e0bb6..59291c20ce 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 6c5450aacd..2b644ae98a 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 4a3adcb405..e5455dc8c4 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 2838bab1a8..a848ce7d90 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 1c83fc243b..221214eff4 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 f8739f1c82..adf6d7984b 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 98818bcd61..9094d7cc5d 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(); - - winrt::hstring title; - - switch (eventArgs.Level()) + auto weakThis = get_weak(); + co_await winrt::resume_foreground(Dispatcher()); + if (auto page = weakThis.get()) { - 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; - } + winrt::hstring message = eventArgs.Message(); - _ShowControlNoticeDialog(title, message); + 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; + } + + 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 060a45f99a..8ff980e24e 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 2481fb6d32..b61a2eb605 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 769b9e333e..b248dab350 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 0000000000..46775a7031 --- /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 0000000000..8ab58a2243 --- /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 0000000000..c3074000f7 --- /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 0000000000..ecee7cc0e5 --- /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 0000000000..cd099dff3a --- /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 0000000000..3a9afaa140 --- /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 3586f0314a..f4b4d9bc78 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 dfc31ad441..92b60f393d 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 4fe7ff8911..dae093f51f 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 0000000000..a21c0747c3 --- /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 79dab5af89..5346d0507c 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>(); + _interactivity = winrt::make_self(settings, connection); + _core = _interactivity->GetCore(); - // GH#8969: pre-seed working directory to prevent potential races - _terminal->SetWorkingDirectory(_settings.StartingDirectory()); + // Use a manual revoker on the output event, so we can immediately stop + // worrying about it on destruction. + _coreOutputEventToken = _core->ReceivedOutput({ this, &TermControl::_coreReceivedOutput }); - auto pfnWarningBell = [this]() { - _playWarningBell->Run(); - }; - _terminal->SetWarningBellCallback(pfnWarningBell); + // 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 }); - auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1); - _terminal->SetTitleChangedCallback(pfnTitleChanged); + // This event is specifically triggered by the renderer thread, a BG thread. Use a weak ref here. + _core->RendererEnteredErrorState({ get_weak(), &TermControl::_RendererEnteredErrorState }); - 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); - }); + // 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; + } - if (_initializedTerminal) - { - return false; - } + const auto panelWidth = SwapChainPanel().ActualWidth(); + const auto panelHeight = SwapChainPanel().ActualHeight(); + const auto panelScaleX = SwapChainPanel().CompositionScaleX(); + const auto panelScaleY = SwapChainPanel().CompositionScaleY(); - const auto actualWidth = SwapChainPanel().ActualWidth(); - const auto actualHeight = SwapChainPanel().ActualHeight(); + const auto windowWidth = panelWidth * panelScaleX; + const auto windowHeight = panelHeight * panelScaleY; - const auto windowWidth = actualWidth * SwapChainPanel().CompositionScaleX(); // Width() and Height() are NaN? - const auto windowHeight = actualHeight * SwapChainPanel().CompositionScaleY(); + if (windowWidth == 0 || windowHeight == 0) + { + return false; + } - if (windowWidth == 0 || windowHeight == 0) - { - return false; - } + // 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 }); - // 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(); + const auto coreInitialized = _core->Initialize(panelWidth, + panelHeight, + panelScaleX); + if (!coreInitialized) + { + return false; + } + _interactivity->Initialize(); - // 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; + _AttachDxgiSwapChainToXaml(_core->GetSwapChain()); - _renderer->SetRendererEnteredErrorStateCallback([weakThis = get_weak()]() { - if (auto strongThis{ weakThis.get() }) - { - strongThis->_RendererEnteredErrorState(); - } - }); + // 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 }); - THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); + // !! 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(); - // Set up the DX Engine - auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); - _renderer->AddRenderEngine(dxEngine.get()); + auto bufferHeight = _core->BufferHeight(); - // 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); + 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 - const COORD windowSize{ static_cast(windowWidth), static_cast(windowHeight) }; + // 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; + } - // 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() })); + // 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; + } - const auto vp = dxEngine->GetViewportInCharacters(viewInPixels); - const auto width = vp.Width(); - const auto height = vp.Height(); - _connection.Resize(height, width); + // Now that the renderer is set up, update the appearance for initialization + _UpdateAppearanceFromUIThread(_settings); - // Override the default width and height to match the size of the swapChainPanel - _settings.InitialCols(width); - _settings.InitialRows(height); + // 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); - _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); - - _AttachDxgiSwapChainToXaml(_renderEngine->GetSwapChain().Get()); - - // 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)); - - auto bottom = _terminal->GetViewport().BottomExclusive(); - auto bufferHeight = bottom; - - 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 - - localPointerToThread->EnablePainting(); - - // 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; - } - - // 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; - } - - // import value from WinUser (convert from milli-seconds to micro-seconds) - _multiClickTimer = GetDoubleClickTime() * 1000; - - // 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); - - // Now that the renderer is set up, update the appearance for initialization - _UpdateAppearanceFromUIThreadUnderLock(_settings); - - _initializedTerminal = true; - } // scope for TerminalLock - - // 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) - { - 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); - } - } - } - else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) + if (type == Windows::Devices::Input::PointerDeviceType::Touch) { const auto contactRect = point.Properties().ContactRect(); - // Set our touch rect, to start a pan. - _touchAnchor = winrt::Windows::Foundation::Point{ contactRect.X, contactRect.Y }; + auto anchor = til::point{ til::math::rounding, contactRect.X, contactRect.Y }; + _interactivity->TouchPressed(anchor); + } + else + { + 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()) + _interactivity->PointerMoved(TermControl::GetPressedMouseButtons(point), + TermControl::GetPointerUpdateKind(point), + ControlKeyStates(args.KeyModifiers()), + _focused, + pixelPosition); + + if (_focused && point.Properties().IsLeftButtonPressed()) { - _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); - 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(); + til::point newTouchPoint{ til::math::rounding, contactRect.X, contactRect.Y }; - // 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()); - - // 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); + TerminalInput::MouseButtonState state{ leftButtonDown, + midButtonDown, + rightButtonDown }; + return _interactivity->MouseWheel(modifiers, delta, _toTerminalOrigin(location), 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(); - } - - // 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()) + co_await resume_foreground(Dispatcher()); + try { - 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) - { - _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(); + const auto scaleX = sender.CompositionScaleX(); - // 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 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)) { + _core->ReceivedOutput(_coreOutputEventToken); + _RestorePointerCursorHandlers(*this, nullptr); - // Stop accepting new output and state changes before we disconnect everything. - _connection.TerminalOutput(_connectionOutputEventToken); - _connectionStateChangedRevoker.revoke(); - - 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,90 +2364,15 @@ 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)); - } - - winrt::fire_and_forget TermControl::_RaiseReadOnlyWarning() - { - 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)); - } - } - - // 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) - { - if (terminalPosition == _lastHoveredCell) - { - return; - } - - _lastHoveredCell = terminalPosition; - - 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. - - 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)); - } - - 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)) - { - auto lock = _terminal->LockForWriting(); - _lastHoveredId = newId; - _lastHoveredInterval = newInterval; - _renderEngine->UpdateHyperlinkHoveredId(newId); - _renderer->UpdateLastHoveredInterval(newInterval); - _renderer->TriggerRedrawAll(); - } + _core->ToggleReadOnlyMode(); + _ReadOnlyChangedHandlers(*this, winrt::box_value(_core->IsInReadOnlyMode())); } // Method Description: @@ -3484,12 +2383,113 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - args: event data void TermControl::_PointerExitedHandler(Windows::Foundation::IInspectable const& /*sender*/, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& /*e*/) { - _UpdateHoveredCell(std::nullopt); + _core->UpdateHoveredCell(std::nullopt); } - // -------------------------------- 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); + winrt::fire_and_forget TermControl::_hoveredHyperlinkChanged(IInspectable sender, + IInspectable args) + { + auto weakThis{ get_weak() }; + co_await resume_foreground(Dispatcher()); + if (auto self{ weakThis.get() }) + { + 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)); + } + } + } + } + + 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); + } + + 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); + } + + TerminalInput::MouseButtonState TermControl::GetPressedMouseButtons(const winrt::Windows::UI::Input::PointerPoint point) + { + return TerminalInput::MouseButtonState{ point.Properties().IsLeftButtonPressed(), + point.Properties().IsMiddleButtonPressed(), + point.Properties().IsRightButtonPressed() }; + } + + unsigned int TermControl::GetPointerUpdateKind(const winrt::Windows::UI::Input::PointerPoint point) + { + const auto props = point.Properties(); + + // Which mouse button changed state (and how) + unsigned int uiButton{}; + switch (props.PointerUpdateKind()) + { + 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; + } + + void TermControl::_coreWarningBell(const IInspectable& /*sender*/, const IInspectable& /*args*/) + { + _playWarningBell->Run(); + } } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 0ebbc9186e..54fd49c37f 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); + // clang-format off + WINRT_CALLBACK(FontSizeChanged, Control::FontSizeChangedEventArgs); - 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); + 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(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); + 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; + + winrt::com_ptr _core; + winrt::com_ptr _interactivity; + bool _initializedTerminal; winrt::com_ptr _searchBox; - event_token _connectionOutputEventToken; - TerminalConnection::ITerminalConnection::StateChanged_revoker _connectionStateChangedRevoker; - - std::unique_ptr<::Microsoft::Terminal::Core::Terminal> _terminal; - - 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 _TerminalTabColorChanged(const std::optional color); + + 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 560abc8b23..6242436eb0 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 f6a629945e..6df09f3022 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 e2488e2f19..5220de3e4d 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 307f03b116..c2c7c54ca1 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 ff73855f94..ac3c4820da 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 2efd3ab51d..9516f73207 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -196,9 +196,9 @@ public: 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 @@ public: 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 @@ public: 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 891e251ff6..f58a143a10 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 af8167ea01..c8c877032a 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 8c85d1b6ad..5ebe26096c 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 515b2f30d0..96617b513a 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"This test is a placeholder while the rest of this test library is being authored."); - VERIFY_IS_TRUE(true); + 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() + { + 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 0000000000..eeb9825475 --- /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 0000000000..14b024df10 --- /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 0000000000..06392d440d --- /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 8239e461c1..7568822f28 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 a151a59402..a67eba4f3d 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 0ffbc1584e..1763f3c5b5 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 39cbfbf896..7de6832146 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 3da64494fc..48cb13a39e 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 17c51ceecc..19507741d0 100644 --- a/tools/tests.xml +++ b/tools/tests.xml @@ -6,8 +6,8 @@ - - + +