// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "TermControl.h" #include #include #include #include #include #include #include #include "..\..\types\inc\GlyphWidth.hpp" #include "TermControl.g.cpp" #include "TermControlAutomationPeer.h" using namespace ::Microsoft::Console::Types; using namespace ::Microsoft::Terminal::Core; using namespace winrt::Windows::UI::Xaml; using namespace winrt::Windows::UI::Xaml::Input; using namespace winrt::Windows::UI::Xaml::Automation::Peers; using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::System; using namespace winrt::Microsoft::Terminal::Settings; using namespace winrt::Windows::ApplicationModel::DataTransfer; namespace winrt::Microsoft::Terminal::TerminalControl::implementation { // Helper static function to ensure that all ambiguous-width glyphs are reported as narrow. // See microsoft/terminal#2066 for more info. static bool _IsGlyphWideForceNarrowFallback(const std::wstring_view /* glyph */) { return false; // glyph is not wide. } static bool _EnsureStaticInitialization() { // use C++11 magic statics to make sure we only do this once. static bool initialized = []() { // *** THIS IS A SINGLETON *** SetGlyphWidthFallback(_IsGlyphWideForceNarrowFallback); return true; }(); return initialized; } TermControl::TermControl() : TermControl(Settings::TerminalSettings{}, TerminalConnection::ITerminalConnection{ nullptr }) { } TermControl::TermControl(Settings::IControlSettings settings, TerminalConnection::ITerminalConnection connection) : _connection{ connection }, _initializedTerminal{ false }, _settings{ settings }, _closing{ false }, _isTerminalInitiatedScroll{ false }, _autoScrollVelocity{ 0 }, _autoScrollingPointerPoint{ std::nullopt }, _autoScrollTimer{}, _lastAutoScrollUpdateTime{ std::nullopt }, _desiredFont{ DEFAULT_FONT_FACE, 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8 }, _actualFont{ DEFAULT_FONT_FACE, 0, 10, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false }, _touchAnchor{ std::nullopt }, _cursorTimer{}, _lastMouseClick{}, _lastMouseClickPos{}, _searchBox{ nullptr }, _unfocusedClickPos{ std::nullopt }, _isClickDragSelection{ false } { _EnsureStaticInitialization(); InitializeComponent(); // Initialize the terminal only once the swapchainpanel is loaded - that // way, we'll be able to query the real pixel size it got on layout _layoutUpdatedRevoker = SwapChainPanel().LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) { // This event fires every time the layout changes, but it is always the last one to fire // in any layout change chain. That gives us great flexibility in finding the right point // at which to initialize our renderer (and our terminal). // Any earlier than the last layout update and we may not know the terminal's starting size. if (_InitializeTerminal()) { // Only let this succeed once. _layoutUpdatedRevoker.revoke(); } }); // 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); }); _ApplyUISettings(); } // Method Description: // - Loads the search box from the xaml UI and focuses it. void TermControl::CreateSearchBoxControl() { // Lazy load the search box control. if (auto loadedSearchBox{ FindName(L"SearchBox") }) { if (auto searchBox{ loadedSearchBox.try_as<::winrt::Microsoft::Terminal::TerminalControl::SearchBoxControl>() }) { // get at its private implementation _searchBox.copy_from(winrt::get_self(searchBox)); _searchBox->Visibility(Visibility::Visible); _searchBox->SetFocusOnTextbox(); } } } // 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 TermControl::_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(); } } // Method Description: // - The handler for the close button or pressing "Esc" when focusing on the // search dialog. // Arguments: // - IInspectable: not used // - RoutedEventArgs: not used // Return Value: // - void TermControl::_CloseSearchBoxControl(const winrt::Windows::Foundation::IInspectable& /*sender*/, RoutedEventArgs const& /*args*/) { _searchBox->Visibility(Visibility::Collapsed); // Set focus back to terminal control this->Focus(FocusState::Programmatic); } // Method Description: // - Given new settings for this profile, applies the settings to the current terminal. // Arguments: // - newSettings: New settings values for the profile in this terminal. // Return Value: // - winrt::fire_and_forget TermControl::UpdateSettings(Settings::IControlSettings newSettings) { _settings = newSettings; auto weakThis{ get_weak() }; // Dispatch a call to the UI thread to apply the new settings to the // terminal. co_await winrt::resume_foreground(Dispatcher()); // If 'weakThis' is locked, then we can safely work with 'this' if (auto control{ weakThis.get() }) { // Update our control settings _ApplyUISettings(); // Update DxEngine's SelectionBackground _renderEngine->SetSelectionBackground(_settings.SelectionBackground()); // Update the terminal core with its new Core settings _terminal->UpdateSettings(_settings); // Refresh our font with the renderer _UpdateFont(); const auto width = SwapChainPanel().ActualWidth(); const auto height = SwapChainPanel().ActualHeight(); if (width != 0 && height != 0) { // If the font size changed, or the _swapchainPanel's size changed // for any reason, we'll need to make sure to also resize the // buffer. _DoResize will invalidate everything for us. auto lock = _terminal->LockForWriting(); _DoResize(width, height); } } } // Method Description: // - Style our UI elements based on the values in our _settings, and set up // other control-specific settings. This method will be called whenever // the settings are reloaded. // * Calls _InitializeBackgroundBrush to set up the Xaml brush responsible // for the control's background // * Calls _BackgroundColorChanged to style the background of the control // - Core settings will be passed to the terminal in _InitializeTerminal // Arguments: // - // Return Value: // - void TermControl::_ApplyUISettings() { _InitializeBackgroundBrush(); uint32_t bg = _settings.DefaultBackground(); _BackgroundColorChanged(bg); // Apply padding as swapChainPanel's margin auto newMargin = _ParseThicknessFromPadding(_settings.Padding()); SwapChainPanel().Margin(newMargin); // Initialize our font information. const auto fontFace = _settings.FontFace(); const short fontHeight = gsl::narrow_cast(_settings.FontSize()); // The font width doesn't terribly matter, we'll only be using the // height to look it up // The other params here also largely don't matter. // The family is only used to determine if the font is truetype or // not, but DX doesn't use that info at all. // The Codepage is additionally not actually used by the DX engine at all. _actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false }; _desiredFont = { _actualFont }; // set TSF Foreground Media::SolidColorBrush foregroundBrush{}; foregroundBrush.Color(ColorRefToColor(_settings.DefaultForeground())); TSFInputControl().Foreground(foregroundBrush); TSFInputControl().Margin(newMargin); // Apply settings for scrollbar if (_settings.ScrollState() == ScrollbarState::Hidden) { // In the scenario where the user has turned off the OS setting to automatically hide scollbars, the // Terminal scrollbar would still be visible; so, we need to set the control's visibility accordingly to // achieve the intended effect. ScrollBar().IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::None); ScrollBar().Visibility(Visibility::Collapsed); } else // (default or Visible) { // Default behavior ScrollBar().IndicatorMode(Controls::Primitives::ScrollingIndicatorMode::MouseIndicator); ScrollBar().Visibility(Visibility::Visible); } // set number of rows to scroll at a time _rowsToScroll = _settings.RowsToScroll(); } // Method Description: // - Set up each layer's brush used to display the control's background. // - Respects the settings for acrylic, background image and opacity from // _settings. // * If acrylic is not enabled, setup a solid color background, otherwise // use bgcolor as acrylic's tint // - Avoids image flickering and acrylic brush redraw if settings are changed // but the appropriate brush is still in place. // - Does not apply background color outside of acrylic mode; // _BackgroundColorChanged must be called to do so. // Arguments: // - // Return Value: // - void TermControl::_InitializeBackgroundBrush() { if (_settings.UseAcrylic()) { // See if we've already got an acrylic background brush // to avoid the flicker when setting up a new one auto acrylic = RootGrid().Background().try_as(); // Instantiate a brush if there's not already one there if (acrylic == nullptr) { acrylic = Media::AcrylicBrush{}; acrylic.BackgroundSource(Media::AcrylicBackgroundSource::HostBackdrop); } // see GH#1082: Initialize background color so we don't get a // fade/flash when _BackgroundColorChanged is called uint32_t color = _settings.DefaultBackground(); winrt::Windows::UI::Color bgColor{}; bgColor.R = GetRValue(color); bgColor.G = GetGValue(color); bgColor.B = GetBValue(color); bgColor.A = 255; acrylic.FallbackColor(bgColor); acrylic.TintColor(bgColor); // Apply brush settings acrylic.TintOpacity(_settings.TintOpacity()); // Apply brush to control if it's not already there if (RootGrid().Background() != acrylic) { RootGrid().Background(acrylic); } } else { Media::SolidColorBrush solidColor{}; RootGrid().Background(solidColor); } if (!_settings.BackgroundImage().empty()) { Windows::Foundation::Uri imageUri{ _settings.BackgroundImage() }; // Check if the image brush is already pointing to the image // in the modified settings; if it isn't (or isn't there), // set a new image source for the brush auto imageSource = BackgroundImage().Source().try_as(); if (imageSource == nullptr || imageSource.UriSource() == nullptr || imageSource.UriSource().RawUri() != imageUri.RawUri()) { // Note that BitmapImage handles the image load asynchronously, // which is especially important since the image // may well be both large and somewhere out on the // internet. Media::Imaging::BitmapImage image(imageUri); BackgroundImage().Source(image); } // Apply stretch, opacity and alignment settings BackgroundImage().Stretch(_settings.BackgroundImageStretchMode()); BackgroundImage().Opacity(_settings.BackgroundImageOpacity()); BackgroundImage().HorizontalAlignment(_settings.BackgroundImageHorizontalAlignment()); BackgroundImage().VerticalAlignment(_settings.BackgroundImageVerticalAlignment()); } else { BackgroundImage().Source(nullptr); } } // Method Description: // - Style the background of the control with the provided background color // Arguments: // - color: The background color to use as a uint32 (aka DWORD COLORREF) // Return Value: // - winrt::fire_and_forget TermControl::_BackgroundColorChanged(const uint32_t color) { auto weakThis{ get_weak() }; co_await winrt::resume_foreground(Dispatcher()); if (auto control{ weakThis.get() }) { const auto R = GetRValue(color); const auto G = GetGValue(color); const auto B = GetBValue(color); winrt::Windows::UI::Color bgColor{}; bgColor.R = R; bgColor.G = G; bgColor.B = B; bgColor.A = 255; if (auto acrylic = RootGrid().Background().try_as()) { acrylic.FallbackColor(bgColor); acrylic.TintColor(bgColor); } else if (auto solidColor = RootGrid().Background().try_as()) { solidColor.Color(bgColor); } // Set the default background as transparent to prevent the // DX layer from overwriting the background image or acrylic effect _settings.DefaultBackground(ARGB(0, R, G, B)); } } TermControl::~TermControl() { Close(); } // Method Description: // - Creates an automation peer for the Terminal Control, enabling accessibility on our control. // Arguments: // - None // Return Value: // - The automation peer for our control Windows::UI::Xaml::Automation::Peers::AutomationPeer TermControl::OnCreateAutomationPeer() try { if (GetUiaData()) { // 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); _uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(autoPeer.get()); _renderer->AddRenderEngine(_uiaEngine.get()); return *autoPeer; } return nullptr; } catch (...) { LOG_CAUGHT_EXCEPTION(); return nullptr; } ::Microsoft::Console::Types::IUiaData* TermControl::GetUiaData() const { return _terminal.get(); } const FontInfo TermControl::GetActualFont() const { return _actualFont; } const Windows::UI::Xaml::Thickness TermControl::GetPadding() { return SwapChainPanel().Margin(); } TerminalConnection::ConnectionState TermControl::ConnectionState() const { return _connection.State(); } winrt::fire_and_forget TermControl::SwapChainChanged() { if (!_initializedTerminal) { return; } auto chain = _renderEngine->GetSwapChain(); auto weakThis{ get_weak() }; co_await winrt::resume_foreground(Dispatcher()); // If 'weakThis' is locked, then we can safely work with 'this' if (auto control{ weakThis.get() }) { if (_terminal) { auto lock = _terminal->LockForWriting(); auto nativePanel = SwapChainPanel().as(); nativePanel->SetSwapChain(chain.Get()); } } } winrt::fire_and_forget TermControl::_SwapChainRoutine() { auto chain = _renderEngine->GetSwapChain(); auto weakThis{ get_weak() }; co_await winrt::resume_foreground(Dispatcher()); if (auto control{ weakThis.get() }) { if (_terminal) { auto lock = _terminal->LockForWriting(); auto nativePanel = SwapChainPanel().as(); nativePanel->SetSwapChain(chain.Get()); } } } bool TermControl::_InitializeTerminal() { if (_initializedTerminal) { return false; } const auto windowWidth = SwapChainPanel().ActualWidth(); // Width() and Height() are NaN? const auto windowHeight = SwapChainPanel().ActualHeight(); if (windowWidth == 0 || windowHeight == 0) { return false; } _terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>(); // First create the render thread. // Then stash a local pointer to the render thread so we can initialize it and enable it // to paint itself *after* we hand off its ownership to the renderer. // We split up construction and initialization of the render thread object this way // because the renderer and render thread have circular references to each other. auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>(); auto* const localPointerToThread = renderThread.get(); // Now create the renderer and initialize the render thread. _renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread)); ::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer; THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); // Set up the DX Engine auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); _renderer->AddRenderEngine(dxEngine.get()); // Initialize our font with the renderer // We don't have to care about DPI. We'll get a change message immediately if it's not 96 // and react accordingly. _UpdateFont(true); const COORD windowSize{ static_cast(windowWidth), static_cast(windowHeight) }; // Fist set up the dx engine with the window size in pixels. // Then, using the font, get the number of characters that can fit. // Resize our terminal connection to match that size, and initialize the terminal with that size. const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize); THROW_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); // Update DxEngine's SelectionBackground dxEngine->SetSelectionBackground(_settings.SelectionBackground()); const auto vp = dxEngine->GetViewportInCharacters(viewInPixels); const auto width = vp.Width(); const auto height = vp.Height(); _connection.Resize(height, width); // Override the default width and height to match the size of the swapChainPanel _settings.InitialCols(width); _settings.InitialRows(height); _terminal->CreateFromSettings(_settings, renderTarget); // Tell the DX Engine to notify us when the swap chain changes. dxEngine->SetCallback(std::bind(&TermControl::SwapChainChanged, this)); // TODO:GH#3927 - Make it possible to hot-reload this setting. Right // here, the setting will only be used when the Terminal is initialized. dxEngine->SetRetroTerminalEffects(_settings.RetroTerminalEffect()); // TODO:GH#3927 - hot-reload this one too // 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; } THROW_IF_FAILED(dxEngine->Enable()); _renderEngine = std::move(dxEngine); // This event is explicitly revoked in the destructor: does not need weak_ref auto onReceiveOutputFn = [this](const hstring str) { _terminal->Write(str); }; _connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn); auto inputFn = std::bind(&TermControl::_SendInputToConnection, this, std::placeholders::_1); _terminal->SetWriteInputCallback(inputFn); _SwapChainRoutine(); // Set up the height of the ScrollViewer and the grid we're using to fake our scrolling height auto bottom = _terminal->GetViewport().BottomExclusive(); auto bufferHeight = bottom; ScrollBar().Maximum(bufferHeight - bufferHeight); ScrollBar().Minimum(0); ScrollBar().Value(0); ScrollBar().ViewportSize(bufferHeight); localPointerToThread->EnablePainting(); auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1); _terminal->SetTitleChangedCallback(pfnTitleChanged); auto pfnBackgroundColorChanged = std::bind(&TermControl::_BackgroundColorChanged, this, std::placeholders::_1); _terminal->SetBackgroundCallback(pfnBackgroundColorChanged); auto pfnScrollPositionChanged = std::bind(&TermControl::_TerminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); _terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged); static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast(1.0 / 30.0 * 1000000)); _autoScrollTimer.Interval(AutoScrollUpdateInterval); _autoScrollTimer.Tick({ get_weak(), &TermControl::_UpdateAutoScroll }); // Set up blinking cursor int blinkTime = GetCaretBlinkTime(); if (blinkTime != INFINITE) { // Create a timer _cursorTimer = std::make_optional(DispatcherTimer()); _cursorTimer.value().Interval(std::chrono::milliseconds(blinkTime)); _cursorTimer.value().Tick({ get_weak(), &TermControl::_BlinkCursor }); _cursorTimer.value().Start(); } else { // The user has disabled cursor blinking _cursorTimer = std::nullopt; } // import value from WinUser (convert from milli-seconds to micro-seconds) _multiClickTimer = GetDoubleClickTime() * 1000; // 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); _connection.Start(); _initializedTerminal = true; _InitializedHandlers(*this, nullptr); return true; } void TermControl::_CharacterHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, Input::CharacterReceivedRoutedEventArgs const& e) { if (_closing) { return; } const auto ch = e.Character(); const bool handled = _terminal->SendCharEvent(ch); e.Handled(handled); } // Method Description: // - Manually generate an F7 event into the key bindings or terminal. // This is required as part of GH#638. // Return value: // - Whether F7 was handled. bool TermControl::OnF7Pressed() { bool handled{ false }; auto bindings{ _settings.KeyBindings() }; const auto modifiers{ _GetPressedModifierKeys() }; if (bindings) { handled = bindings.TryKeyChord({ modifiers.IsCtrlPressed(), modifiers.IsAltPressed(), modifiers.IsShiftPressed(), VK_F7, }); } if (!handled) { // _TrySendKeyEvent pretends it didn't handle F7 for some unknown reason. (void)_TrySendKeyEvent(VK_F7, 0, modifiers); handled = true; } return handled; } void TermControl::_KeyDownHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, Input::KeyRoutedEventArgs const& e) { // If the current focused element is a child element of searchbox, // we do not send this event up to terminal if (_searchBox && _searchBox->ContainsFocus()) { return; } // mark event as handled and do nothing if... // - closing // - key modifier is pressed // NOTE: for key combos like CTRL + C, two events are fired (one for CTRL, one for 'C'). We care about the 'C' event and then check for key modifiers below. if (_closing || e.OriginalKey() == VirtualKey::Control || e.OriginalKey() == VirtualKey::Shift || e.OriginalKey() == VirtualKey::Menu || e.OriginalKey() == VirtualKey::LeftWindows || e.OriginalKey() == VirtualKey::RightWindows) { e.Handled(true); return; } const auto modifiers = _GetPressedModifierKeys(); const auto vkey = gsl::narrow_cast(e.OriginalKey()); const auto scanCode = gsl::narrow_cast(e.KeyStatus().ScanCode); bool handled = false; // GH#2235: Terminal::Settings hasn't been modified to differentiate between AltGr and Ctrl+Alt yet. // -> Don't check for key bindings if this is an AltGr key combination. if (!modifiers.IsAltGrPressed()) { auto bindings = _settings.KeyBindings(); if (bindings) { handled = bindings.TryKeyChord({ modifiers.IsCtrlPressed(), modifiers.IsAltPressed(), modifiers.IsShiftPressed(), vkey, }); } } if (!handled) { handled = _TrySendKeyEvent(vkey, scanCode, modifiers); } // Manually prevent keyboard navigation with tab. We want to send tab to // the terminal, and we don't want to be able to escape focus of the // control with tab. if (e.OriginalKey() == VirtualKey::Tab) { handled = true; } e.Handled(handled); } // Method Description: // - Send this particular key event to the terminal. // See Terminal::SendKeyEvent for more information. // - Clears the current selection. // - Makes the cursor briefly visible during typing. // Arguments: // - vkey: The vkey of the key pressed. // - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. bool TermControl::_TrySendKeyEvent(const WORD vkey, const WORD scanCode, const ControlKeyStates modifiers) { // 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. if (_terminal->IsSelectionActive()) { _terminal->ClearSelection(); _renderer->TriggerSelection(); if (vkey == VK_ESCAPE) { return true; } } if (vkey == VK_ESCAPE || vkey == VK_RETURN) { TSFInputControl().ClearBuffer(); } // 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) : true; if (_cursorTimer.has_value()) { // Manually show the cursor when a key is pressed. Restarting // the timer prevents flickering. _terminal->SetCursorVisible(true); _cursorTimer.value().Start(); } return handled; } // Method Description: // - handle a tap event by taking focus // Arguments: // - sender: the XAML element responding to the tap event // - args: event data void TermControl::_TappedHandler(const IInspectable& /*sender*/, const TappedRoutedEventArgs& e) { Focus(FocusState::Pointer); e.Handled(true); } // Method Description: // - handle a mouse click event. Begin selection process. // Arguments: // - sender: the XAML element responding to the pointer input // - args: event data void TermControl::_PointerPressedHandler(Windows::Foundation::IInspectable const& sender, Input::PointerRoutedEventArgs const& args) { _CapturePointer(sender, args); const auto ptr = args.Pointer(); const auto point = args.GetCurrentPoint(*this); if (!_focused) { Focus(FocusState::Pointer); // Save the click position here when the terminal does not have focus // because they might be performing a click-drag selection. Since we // only want to start the selection when the user moves the pointer with // the left mouse button held down, the PointerMovedHandler will use // this saved position to set the SelectionAnchor. _unfocusedClickPos = point.Position(); } 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)); if (point.Properties().IsLeftButtonPressed()) { // _unfocusedClickPos having a value signifies to us that // the user clicked on an unfocused terminal. We don't want // a single left click from out of focus to start a selection, // so we return fast here. if (_unfocusedClickPos) { args.Handled(true); return; } const auto cursorPosition = point.Position(); const auto terminalPosition = _GetTerminalPosition(cursorPosition); // handle ALT key _terminal->SetBlockSelection(altEnabled); auto clickCount = _NumberOfClicks(cursorPosition, point.Timestamp()); // This formula enables the number of clicks to cycle properly between single-, double-, and triple-click. // To increase the number of acceptable click states, simply increment MAX_CLICK_COUNT and add another if-statement const unsigned int MAX_CLICK_COUNT = 3; const auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ((clickCount + MAX_CLICK_COUNT - 1) % MAX_CLICK_COUNT) + 1 : clickCount; if (multiClickMapper == 3) { _terminal->MultiClickSelection(terminalPosition, ::Terminal::SelectionExpansionMode::Line); } else if (multiClickMapper == 2) { _terminal->MultiClickSelection(terminalPosition, ::Terminal::SelectionExpansionMode::Word); } else { if (shiftEnabled && _terminal->IsSelectionActive()) { _terminal->SetSelectionEnd(terminalPosition, ::Terminal::SelectionExpansionMode::Cell); } else { // save location before rendering _terminal->SetSelectionAnchor(terminalPosition); } _lastMouseClick = point.Timestamp(); _lastMouseClickPos = cursorPosition; } _renderer->TriggerSelection(); } else if (point.Properties().IsRightButtonPressed()) { // copyOnSelect causes right-click to always paste if (_terminal->IsCopyOnSelectActive() || !_terminal->IsSelectionActive()) { PasteTextFromClipboard(); } else { CopySelectionToClipboard(!shiftEnabled); } } } else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) { const auto contactRect = point.Properties().ContactRect(); // Set our touch rect, to start a pan. _touchAnchor = winrt::Windows::Foundation::Point{ contactRect.X, contactRect.Y }; } args.Handled(true); } // Method Description: // - handle a mouse moved event. Specifically handling mouse drag to update selection process. // Arguments: // - sender: not used // - args: event data void TermControl::_PointerMovedHandler(Windows::Foundation::IInspectable const& /*sender*/, Input::PointerRoutedEventArgs const& args) { const auto ptr = args.Pointer(); const auto point = args.GetCurrentPoint(*this); if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) { if (point.Properties().IsLeftButtonPressed()) { _isClickDragSelection = true; // If this does not have a value, it means that PointerPressedHandler already // set the SelectionAnchor. If it does have a value, that means the user is // performing a click-drag selection on an unfocused terminal, so // a SelectionAnchor isn't set yet. We'll have to set it here. if (_unfocusedClickPos) { _terminal->SetSelectionAnchor(_GetTerminalPosition(*_unfocusedClickPos)); } const auto cursorPosition = point.Position(); _SetEndSelectionPointAtCursor(cursorPosition); const double cursorBelowBottomDist = cursorPosition.Y - SwapChainPanel().Margin().Top - SwapChainPanel().ActualHeight(); const double cursorAboveTopDist = -1 * cursorPosition.Y + SwapChainPanel().Margin().Top; constexpr double MinAutoScrollDist = 2.0; // Arbitrary value double newAutoScrollVelocity = 0.0; if (cursorBelowBottomDist > MinAutoScrollDist) { newAutoScrollVelocity = _GetAutoScrollSpeed(cursorBelowBottomDist); } else if (cursorAboveTopDist > MinAutoScrollDist) { newAutoScrollVelocity = -1.0 * _GetAutoScrollSpeed(cursorAboveTopDist); } if (newAutoScrollVelocity != 0) { _TryStartAutoScroll(point, newAutoScrollVelocity); } else { _TryStopAutoScroll(ptr.PointerId()); } } } else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch && _touchAnchor) { const auto contactRect = point.Properties().ContactRect(); winrt::Windows::Foundation::Point newTouchPoint{ contactRect.X, contactRect.Y }; const auto anchor = _touchAnchor.value(); // Get the difference between the point we've dragged to and the start of the touch. const float fontHeight = float(_actualFont.GetSize().Y); const float dy = newTouchPoint.Y - anchor.Y; // Start viewport scroll after we've moved more than a half row of text if (std::abs(dy) > (fontHeight / 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 / fontHeight); 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; } } args.Handled(true); } // Method Description: // - Event handler for the PointerReleased event. We use this to de-anchor // touch events, to stop scrolling via touch. // Arguments: // - sender: the XAML element responding to the pointer input // - args: event data void TermControl::_PointerReleasedHandler(Windows::Foundation::IInspectable const& sender, Input::PointerRoutedEventArgs const& args) { _ReleasePointerCapture(sender, args); const auto ptr = args.Pointer(); if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Mouse || ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Pen) { const auto modifiers = static_cast(args.KeyModifiers()); // static_cast to a uint32_t because we can't use the WI_IsFlagSet // macro directly with a VirtualKeyModifiers const auto shiftEnabled = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Shift)); if (_terminal->IsCopyOnSelectActive()) { // If the terminal was in focus, copy to clipboard. // If the terminal was unfocused AND a click-drag selection happened, copy to clipboard. if (!_unfocusedClickPos || (_unfocusedClickPos && _isClickDragSelection)) { CopySelectionToClipboard(!shiftEnabled); } } } else if (ptr.PointerDeviceType() == Windows::Devices::Input::PointerDeviceType::Touch) { _touchAnchor = std::nullopt; } _unfocusedClickPos = std::nullopt; _isClickDragSelection = false; _TryStopAutoScroll(ptr.PointerId()); args.Handled(true); } // Method Description: // - Event handler for the PointerWheelChanged event. This is raised in // response to mouse wheel changes. Depending upon what modifier keys are // pressed, different actions will take place. // Arguments: // - args: the event args containing information about t`he mouse wheel event. void TermControl::_MouseWheelHandler(Windows::Foundation::IInspectable const& /*sender*/, Input::PointerRoutedEventArgs const& args) { const auto point = args.GetCurrentPoint(*this); const auto delta = point.Properties().MouseWheelDelta(); // Get the state of the Ctrl & Shift keys // static_cast to a uint32_t because we can't use the WI_IsFlagSet macro // directly with a VirtualKeyModifiers const auto modifiers = static_cast(args.KeyModifiers()); const auto ctrlPressed = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Control)); const auto shiftPressed = WI_IsFlagSet(modifiers, static_cast(VirtualKeyModifiers::Shift)); if (ctrlPressed && shiftPressed) { _MouseTransparencyHandler(delta); } else if (ctrlPressed) { _MouseZoomHandler(delta); } else { _MouseScrollHandler(delta, point); } } // Method Description: // - Adjust the opacity of the acrylic background in response to a mouse // scrolling event. // Arguments: // - mouseDelta: the mouse wheel delta that triggered this event. void TermControl::_MouseTransparencyHandler(const double mouseDelta) { // Transparency is on a scale of [0.0,1.0], so only increment by .01. const auto effectiveDelta = mouseDelta < 0 ? -.01 : .01; if (_settings.UseAcrylic()) { try { auto acrylicBrush = RootGrid().Background().as(); acrylicBrush.TintOpacity(acrylicBrush.TintOpacity() + effectiveDelta); } CATCH_LOG(); } } // Method Description: // - Adjust the font size of the terminal in response to a mouse scrolling // event. // Arguments: // - mouseDelta: the mouse wheel delta that triggered this event. void TermControl::_MouseZoomHandler(const double mouseDelta) { const auto fontDelta = mouseDelta < 0 ? -1 : 1; AdjustFontSize(fontDelta); } // Method Description: // - Reset the font size of the terminal to its default size. // Arguments: // - none void TermControl::ResetFontSize() { _SetFontSize(_settings.FontSize()); } // Method Description: // - Adjust the font size of the terminal control. // Arguments: // - fontSizeDelta: The amount to increase or decrease the font size by. void TermControl::AdjustFontSize(int fontSizeDelta) { const auto newSize = _desiredFont.GetEngineSize().Y + fontSizeDelta; _SetFontSize(newSize); } // Method Description: // - Scroll the visible viewport in response to a mouse wheel event. // Arguments: // - mouseDelta: the mouse wheel delta that triggered this event. void TermControl::_MouseScrollHandler(const double mouseDelta, Windows::UI::Input::PointerPoint const& pointerPoint) { const auto currentOffset = ScrollBar().Value(); // negative = down, positive = up // However, for us, the signs are flipped. const auto rowDelta = mouseDelta / (-1.0 * WHEEL_DELTA); // With one of the precision mouses, one click is always a multiple of 120, // but the "smooth scrolling" mode results in non-int values double newValue = (_rowsToScroll * rowDelta) + (currentOffset); // The scroll bar's ValueChanged handler will actually move the viewport // for us. ScrollBar().Value(newValue); if (_terminal->IsSelectionActive() && pointerPoint.Properties().IsLeftButtonPressed()) { // If user is mouse selecting and scrolls, they then point at new character. // Make sure selection reflects that immediately. _SetEndSelectionPointAtCursor(pointerPoint.Position()); } } void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/, Controls::Primitives::RangeBaseValueChangedEventArgs const& args) { if (_isTerminalInitiatedScroll) { return; } 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); // We've just told the terminal to update its viewport to reflect the // new scroll value so the scroll bar matches the viewport now. _willUpdateScrollBarToMatchViewport.store(false); } // Method Description: // - captures the pointer so that none of the other XAML elements respond to pointer events // Arguments: // - sender: XAML element that is interacting with pointer // - args: pointer data (i.e.: mouse, touch) // Return Value: // - true if we successfully capture the pointer, false otherwise. bool TermControl::_CapturePointer(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& args) { IUIElement uielem; if (sender.try_as(uielem)) { uielem.CapturePointer(args.Pointer()); return true; } return false; } // Method Description: // - releases the captured pointer because we're done responding to XAML pointer events // Arguments: // - sender: XAML element that is interacting with pointer // - args: pointer data (i.e.: mouse, touch) // Return Value: // - true if we release capture of the pointer, false otherwise. bool TermControl::_ReleasePointerCapture(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& args) { IUIElement uielem; if (sender.try_as(uielem)) { uielem.ReleasePointerCapture(args.Pointer()); return true; } return false; } // Method Description: // - Starts new pointer related auto scroll behavior, or continues existing one. // Does nothing when there is already auto scroll associated with another pointer. // Arguments: // - pointerPoint: info about pointer that causes auto scroll. Pointer's position // is later used to update selection. // - scrollVelocity: target velocity of scrolling in characters / sec void TermControl::_TryStartAutoScroll(Windows::UI::Input::PointerPoint const& pointerPoint, const double scrollVelocity) { // Allow only one pointer at the time if (!_autoScrollingPointerPoint.has_value() || _autoScrollingPointerPoint.value().PointerId() == pointerPoint.PointerId()) { _autoScrollingPointerPoint = pointerPoint; _autoScrollVelocity = scrollVelocity; // If this is first time the auto scroll update is about to be called, // kick-start it by initializing its time delta as if it started now if (!_lastAutoScrollUpdateTime.has_value()) { _lastAutoScrollUpdateTime = std::chrono::high_resolution_clock::now(); } // Apparently this check is not necessary but greatly improves performance if (!_autoScrollTimer.IsEnabled()) { _autoScrollTimer.Start(); } } } // Method Description: // - Stops auto scroll if it's active and is associated with supplied pointer id. // Arguments: // - pointerId: id of pointer for which to stop auto scroll void TermControl::_TryStopAutoScroll(const uint32_t pointerId) { if (_autoScrollingPointerPoint.has_value() && pointerId == _autoScrollingPointerPoint.value().PointerId()) { _autoScrollingPointerPoint = std::nullopt; _autoScrollVelocity = 0; _lastAutoScrollUpdateTime = std::nullopt; // Apparently this check is not necessary but greatly improves performance if (_autoScrollTimer.IsEnabled()) { _autoScrollTimer.Stop(); } } } // Method Description: // - Called continuously to gradually scroll viewport when user is // mouse selecting outside it (to 'follow' the cursor). // Arguments: // - none void TermControl::_UpdateAutoScroll(Windows::Foundation::IInspectable const& /* sender */, Windows::Foundation::IInspectable const& /* e */) { if (_autoScrollVelocity != 0) { const auto timeNow = std::chrono::high_resolution_clock::now(); if (_lastAutoScrollUpdateTime.has_value()) { static constexpr double microSecPerSec = 1000000.0; const double deltaTime = std::chrono::duration_cast(timeNow - _lastAutoScrollUpdateTime.value()).count() / microSecPerSec; ScrollBar().Value(ScrollBar().Value() + _autoScrollVelocity * deltaTime); if (_autoScrollingPointerPoint.has_value()) { _SetEndSelectionPointAtCursor(_autoScrollingPointerPoint.value().Position()); } } _lastAutoScrollUpdateTime = timeNow; } } // Method Description: // - Event handler for the GotFocus event. This is used to... // - enable accessibility notifications for this TermControl // - start blinking the cursor when the window is focused // - update the number of lines to scroll to the value set in the system void TermControl::_GotFocusHandler(Windows::Foundation::IInspectable const& /* sender */, RoutedEventArgs const& /* args */) { if (_closing) { return; } _focused = true; // If the searchbox is focused, we don't want TSFInputControl to think // it has focus so it doesn't intercept IME input. We also don't want the // terminal's cursor to start blinking. So, we'll just return quickly here. if (_searchBox && _searchBox->ContainsFocus()) { return; } if (_uiaEngine.get()) { THROW_IF_FAILED(_uiaEngine->Enable()); } if (TSFInputControl() != nullptr) { TSFInputControl().NotifyFocusEnter(); } if (_cursorTimer.has_value()) { // When the terminal focuses, show the cursor immediately _terminal->SetCursorVisible(true); _cursorTimer.value().Start(); } _rowsToScroll = _settings.RowsToScroll(); } // Method Description: // - Event handler for the LostFocus event. This is used to... // - disable accessibility notifications for this TermControl // - hide and stop blinking the cursor when the window loses focus. void TermControl::_LostFocusHandler(Windows::Foundation::IInspectable const& /* sender */, RoutedEventArgs const& /* args */) { if (_closing) { return; } _focused = false; if (_uiaEngine.get()) { THROW_IF_FAILED(_uiaEngine->Disable()); } if (TSFInputControl() != nullptr) { TSFInputControl().NotifyFocusLeave(); } if (_cursorTimer.has_value()) { _cursorTimer.value().Stop(); _terminal->SetCursorVisible(false); } } // 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::_SendInputToConnection(const std::wstring& wstr) { _connection.WriteInput(wstr); } // Method Description: // - Pre-process text pasted (presumably from the clipboard) // before sending it over the terminal's connection, converting // Windows-space \r\n line-endings to \r line-endings void TermControl::_SendPastedTextToConnection(const std::wstring& wstr) { // Some notes on this implementation: // // - std::regex can do this in a single line, but is somewhat // overkill for a simple search/replace operation (and its // performance guarantees aren't exactly stellar) // - The STL doesn't have a simple string search/replace method. // This fact is lamentable. // - This line-ending conversion is intentionally fairly // conservative, to avoid stripping out lone \n characters // where they could conceivably be intentional. std::wstring stripped{ wstr }; std::wstring::size_type pos = 0; while ((pos = stripped.find(L"\r\n", pos)) != std::wstring::npos) { stripped.replace(pos, 2, L"\r"); } _connection.WriteInput(stripped); _terminal->TrySnapOnInput(); } // Method Description: // - Update the font with the renderer. This will be called either when the // font changes or the DPI changes, as DPI changes will necessitate a // font change. This method will *not* change the buffer/viewport size // to account for the new glyph dimensions. Callers should make sure to // appropriately call _DoResize after this method is called. // 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) { auto lock = _terminal->LockForWriting(); const int newDpi = static_cast(static_cast(USER_DEFAULT_SCREEN_DPI) * SwapChainPanel().CompositionScaleX()); // TODO: MSFT:20895307 If the font doesn't exist, this doesn't // actually fail. We need a way to gracefully fallback. _renderer->TriggerFontChange(newDpi, _desiredFont, _actualFont); 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(); _actualFont = { fontFace, 0, 10, { 0, newSize }, CP_UTF8, false }; _desiredFont = { _actualFont }; // Refresh our font with the renderer _UpdateFont(); // Resize the terminal's BUFFER to match the new font size. This does // NOT change the size of the window, because that can lead to more // problems (like what happens when you change the font size while the // window is maximized?) auto lock = _terminal->LockForWriting(); _DoResize(SwapChainPanel().ActualWidth(), SwapChainPanel().ActualHeight()); } CATCH_LOG(); } // Method Description: // - Triggered when the swapchain changes size. We use this to resize the // terminal buffers to match the new visible size. // Arguments: // - e: a SizeChangedEventArgs with the new dimensions of the SwapChainPanel void TermControl::_SwapChainSizeChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, SizeChangedEventArgs const& e) { if (!_initializedTerminal) { return; } auto lock = _terminal->LockForWriting(); const auto foundationSize = e.NewSize(); _DoResize(foundationSize.Width, foundationSize.Height); } void TermControl::_SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, Windows::Foundation::IInspectable const& /*args*/) { if (_renderEngine) { const auto scale = sender.CompositionScaleX(); const auto dpi = (int)(scale * USER_DEFAULT_SCREEN_DPI); // TODO: MSFT: 21169071 - Shouldn't this all happen through _renderer and trigger the invalidate automatically on DPI change? THROW_IF_FAILED(_renderEngine->UpdateDpi(dpi)); _renderer->TriggerRedrawAll(); } } // Method Description: // - Toggle the cursor on and off when called by the cursor blink timer. // Arguments: // - sender: not used // - e: not used void TermControl::_BlinkCursor(Windows::Foundation::IInspectable const& /* sender */, Windows::Foundation::IInspectable const& /* e */) { if ((_closing) || (!_terminal->IsCursorBlinkingAllowed() && _terminal->IsCursorVisible())) { return; } _terminal->SetCursorVisible(!_terminal->IsCursorVisible()); } // Method Description: // - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging. // Arguments: // - cursorPosition: in pixels, relative to the origin of the control void TermControl::_SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition) { auto terminalPosition = _GetTerminalPosition(cursorPosition); const short lastVisibleRow = std::max(_terminal->GetViewport().Height() - 1, 0); const short lastVisibleCol = std::max(_terminal->GetViewport().Width() - 1, 0); terminalPosition.Y = std::clamp(terminalPosition.Y, 0, lastVisibleRow); terminalPosition.X = std::clamp(terminalPosition.X, 0, lastVisibleCol); // save location (for rendering) + render _terminal->SetSelectionEnd(terminalPosition); _renderer->TriggerSelection(); } // Method Description: // - Process a resize event that was initiated by the user. This can either // be due to the user resizing the window (causing the swapchain to // resize) or due to the DPI changing (causing us to need to resize the // buffer to match) // Arguments: // - newWidth: the new width of the swapchain, in pixels. // - newHeight: the new height of the swapchain, in pixels. void TermControl::_DoResize(const double newWidth, const double newHeight) { SIZE size; size.cx = static_cast(newWidth); size.cy = static_cast(newHeight); // Don't actually resize so small that a single character wouldn't fit // in either dimension. The buffer really doesn't like being size 0. if (size.cx < _actualFont.GetSize().X || size.cy < _actualFont.GetSize().Y) { return; } _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. // TODO: MSFT:20642295 Resizing the buffer will corrupt it // I believe we'll need support for CSI 2J, and additionally I think // we're resetting the viewport to the top const HRESULT hr = _terminal->UserResize({ vp.Width(), vp.Height() }); if (SUCCEEDED(hr) && hr != S_FALSE) { _connection.Resize(vp.Height(), vp.Width()); } } void TermControl::_TerminalTitleChanged(const std::wstring_view& wstr) { _titleChangedHandlers(winrt::hstring{ wstr }); } // Method Description: // - Update the position and size of the scrollbar to match the given // viewport top, viewport height, and buffer size. // The change will be actually handled in _ScrollbarChangeHandler. // This should be done on the UI thread. Make sure the caller is calling // us in a RunAsync block. // Arguments: // - viewTop: the top of the visible viewport, in rows. 0 indicates the top // of the buffer. // - viewHeight: the height of the viewport in rows. // - bufferSize: the length of the buffer, in rows void TermControl::_ScrollbarUpdater(Controls::Primitives::ScrollBar scrollBar, const int viewTop, const int viewHeight, const int bufferSize) { // The terminal is already in the scroll position it wants, so no need // to tell it to scroll. _isTerminalInitiatedScroll = true; const auto hiddenContent = bufferSize - viewHeight; scrollBar.Maximum(hiddenContent); scrollBar.Minimum(0); scrollBar.ViewportSize(viewHeight); scrollBar.Value(viewTop); _isTerminalInitiatedScroll = false; } // 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 winrt::fire_and_forget TermControl::_TerminalScrollPositionChanged(const int viewTop, const int viewHeight, const int bufferSize) { // Since this callback fires from non-UI thread, we might be already // closed/closing. if (_closing.load()) { return; } _scrollPositionChangedHandlers(viewTop, viewHeight, bufferSize); auto weakThis{ get_weak() }; co_await winrt::resume_foreground(Dispatcher()); // Even if we weren't closed/closing few lines above, we might be // while waiting for this block of code to be dispatched. // If 'weakThis' is locked, then we can safely work with 'this' if (auto control{ weakThis.get() }) { if (!_closing.load()) { // Update our scrollbar _ScrollbarUpdater(ScrollBar(), viewTop, viewHeight, bufferSize); } } } hstring TermControl::Title() { if (!_initializedTerminal) return L""; hstring hstr(_terminal->GetConsoleTitle()); return hstr; } hstring TermControl::GetProfileName() const { return _settings.ProfileName(); } // Method Description: // - Given a copy-able selection, get the selected text from the buffer and send it to the // Windows Clipboard (CascadiaWin32:main.cpp). // - CopyOnSelect does NOT clear the selection // Arguments: // - trimTrailingWhitespace: enable removing any whitespace from copied selection // and get text to appear on separate lines. bool TermControl::CopySelectionToClipboard(bool trimTrailingWhitespace) { // no selection --> nothing to copy if (_terminal == nullptr || !_terminal->IsSelectionActive()) { return false; } // extract text from buffer const auto bufferData = _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace); // convert text: vector --> string std::wstring textData; for (const auto& text : bufferData.text) { textData += text; } // convert text to HTML format const auto htmlData = TextBuffer::GenHTML(bufferData, _actualFont.GetUnscaledSize().Y, _actualFont.GetFaceName(), _settings.DefaultBackground(), "Windows Terminal"); // convert to RTF format const auto rtfData = TextBuffer::GenRTF(bufferData, _actualFont.GetUnscaledSize().Y, _actualFont.GetFaceName(), _settings.DefaultBackground()); if (!_terminal->IsCopyOnSelectActive()) { _terminal->ClearSelection(); _renderer->TriggerSelection(); } // send data up for clipboard auto copyArgs = winrt::make_self(winrt::hstring(textData), winrt::to_hstring(htmlData), winrt::to_hstring(rtfData)); _clipboardCopyHandlers(*this, *copyArgs); return true; } // Method Description: // - Initiate a paste operation. void TermControl::PasteTextFromClipboard() { // attach TermControl::_SendInputToConnection() as the clipboardDataHandler. // This is called when the clipboard data is loaded. auto clipboardDataHandler = std::bind(&TermControl::_SendPastedTextToConnection, this, std::placeholders::_1); auto pasteArgs = winrt::make_self(clipboardDataHandler); // send paste event up to TermApp _clipboardPasteHandlers(*this, *pasteArgs); } void TermControl::Close() { if (!_closing.exchange(true)) { // Stop accepting new output and state changes before we disconnect everything. _connection.TerminalOutput(_connectionOutputEventToken); _connectionStateChangedRevoker.revoke(); TSFInputControl().Close(); // Disconnect the TSF input control so it doesn't receive EditContext events. if (auto localConnection{ std::exchange(_connection, nullptr) }) { localConnection.Close(); // connection is destroyed. } if (auto localRenderEngine{ std::exchange(_renderEngine, nullptr) }) { if (auto localRenderer{ std::exchange(_renderer, nullptr) }) { localRenderer->TriggerTeardown(); // renderer is destroyed } // renderEngine is destroyed } if (auto localTerminal{ std::exchange(_terminal, nullptr) }) { _initializedTerminal = false; // terminal is destroyed. } } } // Method Description: // - Scrolls the viewport of the terminal and updates the scroll bar accordingly // Arguments: // - viewTop: the viewTop to scroll to void TermControl::ScrollViewport(int viewTop) { ScrollBar().Value(viewTop); } int TermControl::GetScrollOffset() { return _terminal->GetScrollOffset(); } // Function Description: // - Gets the height of the terminal in lines of text // Return Value: // - The height of the terminal in lines of text int TermControl::GetViewHeight() const { const auto viewPort = _terminal->GetViewport(); return viewPort.Height(); } // Function Description: // - Determines how much space (in pixels) an app would need to reserve to // create a control with the settings stored in the settings param. This // accounts for things like the font size and face, the initialRows and // initialCols, and scrollbar visibility. The returned sized is based upon // the provided DPI value // Arguments: // - settings: A IControlSettings with the settings to get the pixel size of. // - dpi: The DPI we should create the terminal at. This affects things such // as font size, scrollbar and other control scaling, etc. Make sure the // caller knows what monitor the control is about to appear on. // Return Value: // - a point containing the requested dimensions in pixels. winrt::Windows::Foundation::Point TermControl::GetProposedDimensions(IControlSettings const& settings, const uint32_t dpi) { // Initialize our font information. const auto fontFace = settings.FontFace(); const short fontHeight = gsl::narrow_cast(settings.FontSize()); // The font width doesn't terribly matter, we'll only be using the // height to look it up // The other params here also largely don't matter. // The family is only used to determine if the font is truetype or // not, but DX doesn't use that info at all. // The Codepage is additionally not actually used by the DX engine at all. FontInfo actualFont = { fontFace, 0, 10, { 0, fontHeight }, CP_UTF8, false }; FontInfoDesired desiredFont = { actualFont }; // If the settings have negative or zero row or column counts, ignore those counts. // (The lower TerminalCore layer also has upper bounds as well, but at this layer // we may eventually impose different ones depending on how many pixels we can address.) const auto cols = std::max(settings.InitialCols(), 1); const auto rows = std::max(settings.InitialRows(), 1); // Create a DX engine and initialize it with our font and DPI. We'll // then use it to measure how much space the requested rows and columns // will take up. // TODO: MSFT:21254947 - use a static function to do this instead of // instantiating a DxEngine auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); THROW_IF_FAILED(dxEngine->UpdateDpi(dpi)); THROW_IF_FAILED(dxEngine->UpdateFont(desiredFont, actualFont)); const float scale = dxEngine->GetScaling(); const auto fontSize = actualFont.GetSize(); // Manually multiply by the scaling factor. The DX engine doesn't // actually store the scaled font size in the fontInfo.GetSize() // property when the DX engine is in Composition mode (which it is for // the Terminal). At runtime, this is fine, as we'll transform // everything by our scaling, so it'll work out. However, right now we // need to get the exact pixel count. const float fFontWidth = gsl::narrow_cast(fontSize.X * scale); const float fFontHeight = gsl::narrow_cast(fontSize.Y * scale); // UWP XAML scrollbars aren't guaranteed to be the same size as the // ComCtl scrollbars, but it's certainly close enough. const auto scrollbarSize = GetSystemMetricsForDpi(SM_CXVSCROLL, dpi); double width = cols * fFontWidth; // Reserve additional space if scrollbar is intended to be visible if (settings.ScrollState() == ScrollbarState::Visible) { width += scrollbarSize; } double height = rows * fFontHeight; auto thickness = _ParseThicknessFromPadding(settings.Padding()); width += thickness.Left + thickness.Right; height += thickness.Top + thickness.Bottom; return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; } // Method Description: // - Get the size of a single character of this control. The size is in // DIPs. If you need it in _pixels_, you'll need to multiply by the // current display scaling. // Arguments: // - // Return Value: // - The dimensions of a single character of this control, in DIPs winrt::Windows::Foundation::Size TermControl::CharacterDimensions() const { const auto fontSize = _actualFont.GetSize(); return { gsl::narrow_cast(fontSize.X), gsl::narrow_cast(fontSize.Y) }; } // Method Description: // - Get the absolute minimum size that this control can be resized to and // still have 1x1 character visible. This includes the space needed for // the scrollbar and the padding. // Arguments: // - // Return Value: // - The minimum size that this terminal control can be resized to and still // have a visible character. winrt::Windows::Foundation::Size TermControl::MinimumSize() { const auto fontSize = _actualFont.GetSize(); double width = fontSize.X; double height = fontSize.Y; // Reserve additional space if scrollbar is intended to be visible if (_settings.ScrollState() == ScrollbarState::Visible) { width += ScrollBar().ActualWidth(); } // Account for the size of any padding const auto padding = SwapChainPanel().Margin(); width += padding.Left + padding.Right; height += padding.Top + padding.Bottom; return { gsl::narrow_cast(width), gsl::narrow_cast(height) }; } // Method Description: // - Adjusts given dimension (width or height) so that it aligns to the character grid. // The snap is always downward. // Arguments: // - widthOrHeight: if true operates on width, otherwise on height // - dimension: a dimension (width or height) to be snapped // Return Value: // - 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 fontDimension = widthOrHeight ? fontSize.X : fontSize.Y; const auto padding = SwapChainPanel().Margin(); auto nonTerminalArea = gsl::narrow_cast(widthOrHeight ? padding.Left + padding.Right : padding.Top + padding.Bottom); if (widthOrHeight && _settings.ScrollState() == ScrollbarState::Visible) { nonTerminalArea += gsl::narrow_cast(ScrollBar().ActualWidth()); } const auto gridSize = dimension - nonTerminalArea; const int cells = static_cast(gridSize / fontDimension); return cells * fontDimension + nonTerminalArea; } // Method Description: // - Create XAML Thickness object based on padding props provided. // Used for controlling the TermControl XAML Grid container's Padding prop. // Arguments: // - padding: 2D padding values // Single Double value provides uniform padding // Two Double values provide isometric horizontal & vertical padding // Four Double values provide independent padding for 4 sides of the bounding rectangle // Return Value: // - Windows::UI::Xaml::Thickness object Windows::UI::Xaml::Thickness TermControl::_ParseThicknessFromPadding(const hstring padding) { const wchar_t singleCharDelim = L','; std::wstringstream tokenStream(padding.c_str()); std::wstring token; uint8_t paddingPropIndex = 0; std::array thicknessArr = {}; size_t* idx = nullptr; // Get padding values till we run out of delimiter separated values in the stream // or we hit max number of allowable values (= 4) for the bounding rectangle // Non-numeral values detected will default to 0 // std::getline will not throw exception unless flags are set on the wstringstream // std::stod will throw invalid_argument exception if the input is an invalid double value // std::stod will throw out_of_range exception if the input value is more than DBL_MAX try { for (; std::getline(tokenStream, token, singleCharDelim) && (paddingPropIndex < thicknessArr.size()); paddingPropIndex++) { // std::stod internally calls wcstod which handles whitespace prefix (which is ignored) // & stops the scan when first char outside the range of radix is encountered // We'll be permissive till the extent that stod function allows us to be by default // Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail thicknessArr[paddingPropIndex] = std::stod(token, idx); } } catch (...) { // If something goes wrong, even if due to a single bad padding value, we'll reset the index & return default 0 padding paddingPropIndex = 0; LOG_CAUGHT_EXCEPTION(); } switch (paddingPropIndex) { case 1: return ThicknessHelper::FromUniformLength(thicknessArr[0]); case 2: return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[0], thicknessArr[1]); // No case for paddingPropIndex = 3, since it's not a norm to provide just Left, Top & Right padding values leaving out Bottom case 4: return ThicknessHelper::FromLengths(thicknessArr[0], thicknessArr[1], thicknessArr[2], thicknessArr[3]); default: return Thickness(); } } // Method Description: // - Get the modifier keys that are currently pressed. This can be used to // find out which modifiers (ctrl, alt, shift) are pressed in events that // don't necessarily include that state. // Return Value: // - The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states. ControlKeyStates TermControl::_GetPressedModifierKeys() const { CoreWindow window = CoreWindow::GetForCurrentThread(); // DONT USE // != CoreVirtualKeyStates::None // OR // == CoreVirtualKeyStates::Down // Sometimes with the key down, the state is Down | Locked. // Sometimes with the key up, the state is Locked. // IsFlagSet(Down) is the only correct solution. struct KeyModifier { VirtualKey vkey; ControlKeyStates flags; }; constexpr std::array modifiers{ { { VirtualKey::RightMenu, ControlKeyStates::RightAltPressed }, { VirtualKey::LeftMenu, ControlKeyStates::LeftAltPressed }, { VirtualKey::RightControl, ControlKeyStates::RightCtrlPressed }, { VirtualKey::LeftControl, ControlKeyStates::LeftCtrlPressed }, { VirtualKey::Shift, ControlKeyStates::ShiftPressed }, } }; ControlKeyStates flags; for (const auto& mod : modifiers) { const auto state = window.GetKeyState(mod.vkey); const auto isDown = WI_IsFlagSet(state, CoreVirtualKeyStates::Down); if (isDown) { flags |= mod.flags; } } return flags; } // Method Description: // - Gets the corresponding viewport terminal position for the cursor // by excluding the padding and normalizing with the font size. // This is used for selection. // Arguments: // - cursorPosition: the (x,y) position of a given cursor (i.e.: mouse cursor). // NOTE: origin (0,0) is top-left. // Return Value: // - the corresponding viewport terminal position for the given Point parameter const COORD TermControl::_GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition) { // Exclude padding from cursor position calculation COORD terminalPosition = { static_cast(cursorPosition.X - SwapChainPanel().Margin().Left), static_cast(cursorPosition.Y - SwapChainPanel().Margin().Top) }; const auto fontSize = _actualFont.GetSize(); FAIL_FAST_IF(fontSize.X == 0); FAIL_FAST_IF(fontSize.Y == 0); // Normalize to terminal coordinates by using font size terminalPosition.X /= fontSize.X; terminalPosition.Y /= fontSize.Y; return terminalPosition; } // Method Description: // - Composition Completion handler for the TSFInputControl that // handles writing text out to TerminalConnection // Arguments: // - text: the text to write to TerminalConnection // Return Value: // - void TermControl::_CompositionCompleted(winrt::hstring text) { _connection.WriteInput(text); } // Method Description: // - CurrentCursorPosition handler for the TSFInputControl that // handles returning current cursor position. // Arguments: // - eventArgs: event for storing the current cursor position // Return Value: // - void TermControl::_CurrentCursorPositionHandler(const IInspectable& /*sender*/, const CursorPositionEventArgs& eventArgs) { // If we haven't initialized yet, just quick return. if (!_terminal) { return; } const COORD cursorPos = _terminal->GetCursorPosition(); Windows::Foundation::Point p = { gsl::narrow_cast(cursorPos.X), gsl::narrow_cast(cursorPos.Y) }; eventArgs.CurrentPosition(p); } // Method Description: // - FontInfo handler for the TSFInputControl that // handles returning current font information // Arguments: // - eventArgs: event for storing the current font information // Return Value: // - void TermControl::_FontInfoHandler(const IInspectable& /*sender*/, const FontInfoEventArgs& eventArgs) { eventArgs.FontSize(CharacterDimensions()); eventArgs.FontFace(_actualFont.GetFaceName()); } // Method Description: // - Returns the number of clicks that occurred (double and triple click support) // Arguments: // - clickPos: the (x,y) position of a given cursor (i.e.: mouse cursor). // NOTE: origin (0,0) is top-left. // - clickTime: the timestamp that the click occurred // Return Value: // - if the click is in the same position as the last click and within the timeout, the number of clicks within that time window // - otherwise, 1 const unsigned int TermControl::_NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime) { // if click occurred at a different location or past the multiClickTimer... Timestamp delta; THROW_IF_FAILED(UInt64Sub(clickTime, _lastMouseClick, &delta)); if (clickPos != _lastMouseClickPos || delta > _multiClickTimer) { // exit early. This is a single click. _multiClickCounter = 1; } else { _multiClickCounter++; } return _multiClickCounter; } // Method Description: // - Calculates speed of single axis of auto scrolling. It has to allow for both // fast and precise selection. // Arguments: // - cursorDistanceFromBorder: distance from viewport border to cursor, in pixels. Must be non-negative. // Return Value: // - positive speed in characters / sec double TermControl::_GetAutoScrollSpeed(double cursorDistanceFromBorder) const { // The numbers below just feel well, feel free to change. // TODO: Maybe account for space beyond border that user has available return std::pow(cursorDistanceFromBorder, 2.0) / 25.0 + 2.0; } // Method Description: // - Async handler for the "Drop" event. If a file was dropped onto our // root, we'll try to get the path of the file dropped onto us, and write // the full path of the file to our terminal connection. Like conhost, if // the path contains a space, we'll wrap the path in quotes. // - Unlike conhost, if multiple files are dropped onto the terminal, we'll // write all the paths to the terminal, separated by spaces. // Arguments: // - e: The DragEventArgs from the Drop event // Return Value: // - winrt::fire_and_forget TermControl::_DoDragDrop(DragEventArgs const e) { if (e.DataView().Contains(StandardDataFormats::StorageItems())) { auto items = co_await e.DataView().GetStorageItemsAsync(); if (items.Size() > 0) { std::wstring allPaths; for (auto item : items) { // Join the paths with spaces if (!allPaths.empty()) { allPaths += L" "; } std::wstring fullPath{ item.Path() }; const auto containsSpaces = std::find(fullPath.begin(), fullPath.end(), L' ') != fullPath.end(); auto lock = _terminal->LockForWriting(); if (containsSpaces) { fullPath.insert(0, L"\""); fullPath += L"\""; } allPaths += fullPath; } _SendInputToConnection(allPaths); } } } // Method Description: // - Synchronous handler for the "Drop" event. We'll dispatch the async // _DoDragDrop method to handle this, because getting information about // the file that was potentially dropped onto us must be done off the UI // thread. // Arguments: // - e: The DragEventArgs from the Drop event // Return Value: // - void TermControl::_DragDropHandler(Windows::Foundation::IInspectable const& /*sender*/, DragEventArgs const& e) { // Dispatch an async method to handle the drop event. _DoDragDrop(e); } // Method Description: // - Handle the DragOver event. We'll signal that the drag operation we // support is the "copy" operation, and we'll also customize the // appearance of the drag-drop UI, by removing the preview and setting a // custom caption. For more information, see // https://docs.microsoft.com/en-us/windows/uwp/design/input/drag-and-drop#customize-the-ui // Arguments: // - e: The DragEventArgs from the DragOver event // Return Value: // - void TermControl::_DragOverHandler(Windows::Foundation::IInspectable const& /*sender*/, DragEventArgs const& e) { if (!e.DataView().Contains(StandardDataFormats::StorageItems())) { // We can't do anything for non-storageitems right now. return; } // Make sure to set the AcceptedOperation, so that we can later receive the path in the Drop event e.AcceptedOperation(DataPackageOperation::Copy); // Sets custom UI text e.DragUIOverride().Caption(RS_(L"DragFileCaption")); // Sets if the caption is visible e.DragUIOverride().IsCaptionVisible(true); // Sets if the dragged content is visible e.DragUIOverride().IsContentVisible(false); // Sets if the glyph is visibile e.DragUIOverride().IsGlyphVisible(false); } // -------------------------------- WinRT Events --------------------------------- // Winrt events need a method for adding a callback to the event and removing the callback. // These macros will define them both for you. DEFINE_EVENT(TermControl, TitleChanged, _titleChangedHandlers, TerminalControl::TitleChangedEventArgs); DEFINE_EVENT(TermControl, FontSizeChanged, _fontSizeChangedHandlers, TerminalControl::FontSizeChangedEventArgs); DEFINE_EVENT(TermControl, ScrollPositionChanged, _scrollPositionChangedHandlers, TerminalControl::ScrollPositionChangedEventArgs); DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, PasteFromClipboard, _clipboardPasteHandlers, TerminalControl::TermControl, TerminalControl::PasteFromClipboardEventArgs); DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER(TermControl, CopyToClipboard, _clipboardCopyHandlers, TerminalControl::TermControl, TerminalControl::CopyToClipboardEventArgs); // clang-format on }