// 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; // The minimum delay between updates to the scroll bar's values. // The updates are throttled to limit power usage. constexpr const auto ScrollBarUpdateInterval = std::chrono::milliseconds(8); // The minimum delay between updating the TSF input control. constexpr const auto TsfRedrawInterval = std::chrono::milliseconds(100); // The minimum delay between updating the locations of regex patterns constexpr const auto UpdatePatternLocationsInterval = std::chrono::milliseconds(500); 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); // MSFT 33353327: Initialize the renderer in the ctor instead of Initialize(). // We need the renderer to be ready to accept new engines before the SwapChainPanel is ready to go. // If we wait, a screen reader may try to get the AutomationPeer (aka the UIA Engine), and we won't be able to attach // the UIA Engine to the renderer. This prevents us from signaling changes to the cursor or buffer. { // 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)); _renderer->SetRendererEnteredErrorStateCallback([weakThis = get_weak()]() { if (auto strongThis{ weakThis.get() }) { strongThis->_RendererEnteredErrorStateHandlers(*strongThis, nullptr); } }); THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get())); } // Get our dispatcher. If we're hosted in-proc with XAML, this will get // us the same dispatcher as TermControl::Dispatcher(). If we're out of // proc, this'll return null. We'll need to instead make a new // DispatcherQueue (on a new thread), so we can use that for throttled // functions. _dispatcher = winrt::Windows::System::DispatcherQueue::GetForCurrentThread(); if (!_dispatcher) { auto controller{ winrt::Windows::System::DispatcherQueueController::CreateOnDedicatedThread() }; _dispatcher = controller.DispatcherQueue(); } // A few different events should be throttled, so they don't fire absolutely all the time: // * _tsfTryRedrawCanvas: When the cursor position moves, we need to // inform TSF, so it can move the canvas for the composition. We // throttle this so that we're not hopping across the process boundary // every time that the cursor moves. // * _updatePatternLocations: When there's new output, or we scroll the // viewport, we should re-check if there are any visible hyperlinks. // But we don't really need to do this every single time text is // output, we can limit this update to once every 500ms. // * _updateScrollBar: Same idea as the TSF update - we don't _really_ // need to hop across the process boundary every time text is output. // We can throttle this to once every 8ms, which will get us out of // the way of the main output & rendering threads. _tsfTryRedrawCanvas = std::make_shared>( _dispatcher, TsfRedrawInterval, [weakThis = get_weak()]() { if (auto core{ weakThis.get() }; !core->_IsClosing()) { core->_CursorPositionChangedHandlers(*core, nullptr); } }); _updatePatternLocations = std::make_shared>( _dispatcher, UpdatePatternLocationsInterval, [weakThis = get_weak()]() { if (auto core{ weakThis.get() }; !core->_IsClosing()) { core->UpdatePatternLocations(); } }); _updateScrollBar = std::make_shared>( _dispatcher, ScrollBarUpdateInterval, [weakThis = get_weak()](const auto& update) { if (auto core{ weakThis.get() }; !core->_IsClosing()) { core->_ScrollPositionChangedHandlers(*core, update); } }); UpdateSettings(settings); } ControlCore::~ControlCore() { Close(); if (_renderer) { _renderer->TriggerTeardown(); } } 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; } // Set up the DX Engine auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>(); _renderer->AddRenderEngine(dxEngine.get()); _renderEngine = std::move(dxEngine); // 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(_renderEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() })); // Update DxEngine's SelectionBackground _renderEngine->SetSelectionBackground(til::color{ _settings.SelectionBackground() }); const auto vp = _renderEngine->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, *_renderer); // 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! _renderEngine->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) _renderEngine->SetCallback(std::bind(&ControlCore::_renderEngineSwapChainChanged, this)); _renderEngine->SetRetroTerminalEffect(_settings.RetroTerminalEffect()); _renderEngine->SetPixelShaderPath(_settings.PixelShaderPath()); _renderEngine->SetForceFullRepaintRendering(_settings.ForceFullRepaintRendering()); _renderEngine->SetSoftwareRendering(_settings.SoftwareRendering()); _renderEngine->SetIntenseIsBold(_settings.IntenseIsBold()); _updateAntiAliasingMode(_renderEngine.get()); // GH#5098: Inform the engine of the opacity of the default text background. // GH#11315: Always do this, even if they don't have acrylic on. _renderEngine->SetDefaultTextBackgroundOpacity(::base::saturated_cast(_settings.Opacity())); THROW_IF_FAILED(_renderEngine->Enable()); _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) { // Update the selection, if it's present // 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 modify the selection on key _down_. If we // modify 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) { // try to update the selection if (const auto updateSlnParams{ ::Terminal::ConvertKeyEventToUpdateSelectionParams(modifiers, vkey) }) { auto lock = _terminal->LockForWriting(); _terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second); _renderer->TriggerSelection(); return true; } // GH#8791 - don't dismiss selection if Windows key was also pressed as a key-combination. if (!modifiers.IsWinPressed()) { _terminal->ClearSelection(); _renderer->TriggerSelection(); } // 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 (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); _updatePatternLocations->Run(); } void ControlCore::AdjustOpacity(const double adjustment) { if (adjustment == 0) { return; } auto newOpacity = std::clamp(_settings.Opacity() + adjustment, 0.0, 1.0); // GH#5098: Inform the engine of the new opacity of the default text background. SetBackgroundOpacity(::base::saturated_cast(newOpacity)); _settings.Opacity(newOpacity); // GH#11285 - If the user is on Windows 10, and they changed the // transparency of the control s.t. it should be partially opaque, then // opt them in to acrylic. It's the only way to have transparency on // Windows 10. // We'll also turn the acrylic back off when they're fully opaque, which // is what the Terminal did prior to 1.12. if (!IsVintageOpacityAvailable()) { _settings.UseAcrylic(newOpacity < 1.0); } 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(); } // Always redraw after toggling effects. This way even if the control // does not have focus it will update immediately. _renderer->TriggerRedrawAll(); } // 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() { auto lock = _terminal->LockForWriting(); _terminal->UpdatePatternsUnderLock(); } // Method description: // - Updates last hovered cell, renders / removes rendering of hyper-link if required // Arguments: // - terminalPosition: The terminal position of the pointer void ControlCore::SetHoveredCell(Core::Point pos) { _updateHoveredCell(std::optional{ pos }); } void ControlCore::ClearHoveredCell() { _updateHoveredCell(std::nullopt); } 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::HoveredUriText() const { auto lock = _terminal->LockForReading(); // Lock for the duration of our reads. if (_lastHoveredCell.has_value()) { return winrt::hstring{ _terminal->GetHyperlinkAtPosition(*_lastHoveredCell) }; } return {}; } Windows::Foundation::IReference ControlCore::HoveredCell() const { return _lastHoveredCell.has_value() ? Windows::Foundation::IReference{ _lastHoveredCell.value() } : nullptr; } // 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; // GH#11285 - If the user is on Windows 10, and they wanted opacity, but // didn't explicitly request acrylic, then opt them in to acrylic. // On Windows 11+, this isn't needed, because we can have vintage opacity. if (!IsVintageOpacityAvailable() && _settings.Opacity() < 1.0 && !_settings.UseAcrylic()) { _settings.UseAcrylic(true); } // 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 }; _actualFontFaceName = { fontFace }; _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()); _renderEngine->SetIntenseIsBold(_settings.IntenseIsBold()); _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); _terminal->SetFontInfo(_actualFont); if (_renderEngine) { std::unordered_map featureMap; if (const auto fontFeatures = _settings.FontFeatures()) { featureMap.reserve(fontFeatures.Size()); for (const auto& [tag, param] : fontFeatures) { featureMap.emplace(tag, param); } } std::unordered_map axesMap; if (const auto fontAxes = _settings.FontAxes()) { axesMap.reserve(fontAxes.Size()); for (const auto& [axis, value] : fontAxes) { axesMap.emplace(axis, value); } } // TODO: MSFT:20895307 If the font doesn't exist, this doesn't // actually fail. We need a way to gracefully fallback. LOG_IF_FAILED(_renderEngine->UpdateDpi(newDpi)); LOG_IF_FAILED(_renderEngine->UpdateFont(_desiredFont, _actualFont, featureMap, axesMap)); } // 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 }; _actualFontFaceName = { fontFace }; _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; } // 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); const auto currentVP = _terminal->GetViewport(); // Don't actually resize if viewport dimensions didn't change if (vp.Height() == currentVP.Height() && vp.Width() == currentVP.Width()) { 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(); // 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(); _renderer->TriggerSelection(); _terminal->TrySnapOnInput(); } FontInfo ControlCore::GetFont() const { return _actualFont; } winrt::Windows::Foundation::Size ControlCore::FontSize() const noexcept { const auto fontSize = _actualFont.GetSize(); return { ::base::saturated_cast(fontSize.X), ::base::saturated_cast(fontSize.Y) }; } winrt::hstring ControlCore::FontFaceName() const noexcept { // This getter used to return _actualFont.GetFaceName(), however GetFaceName() returns a STL // string and we need to return a WinRT string. This would require an additional allocation. // This method is called 10/s by TSFInputControl at the time of writing. return _actualFontFaceName; } uint16_t ControlCore::FontWeight() const noexcept { return static_cast(_actualFont.GetWeight()); } til::size ControlCore::FontSizeInDips() const { const til::size fontSize{ _actualFont.GetSize() }; return fontSize.scale(til::math::rounding, 1.0f / ::base::saturated_cast(_compositionScale)); } TerminalConnection::ConnectionState ControlCore::ConnectionState() const { return _connection ? _connection.State() : TerminalConnection::ConnectionState::Closed; } 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(); // Start the throttled update of our scrollbar. auto update{ winrt::make(viewTop, viewHeight, bufferSize) }; if (!_inUnitTests) { _updateScrollBar->Run(update); } else { _ScrollPositionChangedHandlers(*this, update); } // Additionally, start the throttled update of where our links are. _updatePatternLocations->Run(); } void ControlCore::_terminalCursorPositionChanged() { // When the buffer's cursor moves, start the throttled func to // eventually dispatch a CursorPositionChanged event. _tsfTryRedrawCanvas->Run(); } void ControlCore::_terminalTaskbarProgressChanged() { _TaskbarProgressChangedHandlers(*this, nullptr); } bool ControlCore::HasSelection() const { return _terminal->IsSelectionActive(); } bool ControlCore::CopyOnSelect() const { return _settings.CopyOnSelect(); } Windows::Foundation::Collections::IVector ControlCore::SelectedText(bool trimTrailingWhitespace) const { // RetrieveSelectedTextFromBuffer will lock while it's reading const auto internalResult{ _terminal->RetrieveSelectedTextFromBuffer(trimTrailingWhitespace).text }; auto result = winrt::single_threaded_vector(); for (const auto& row : internalResult) { result.Append(winrt::hstring{ row }); } return result; } ::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 double opacity) { if (_renderEngine) { auto lock = _terminal->LockForWriting(); _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 (!_IsClosing()) { _closing = 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(); } } uint64_t ControlCore::SwapChainHandle() 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 reinterpret_cast(_renderEngine->GetSwapChainHandle()); } 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::SelectionExpansion mode = ::Terminal::SelectionExpansion::Char; if (numberOfClicks == 1) { mode = ::Terminal::SelectionExpansion::Char; } else if (numberOfClicks == 2) { mode = ::Terminal::SelectionExpansion::Word; } else if (numberOfClicks == 3) { mode = ::Terminal::SelectionExpansion::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::SelectionExpansion::Char || 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) { // _renderer will always exist since it's introduced in the ctor _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); // Start the throttled update of where our hyperlinks are. _updatePatternLocations->Run(); } // Method Description: // - Clear the contents of the buffer. The region cleared is given by // clearType: // * Screen: Clear only the contents of the visible viewport, leaving the // cursor row at the top of the viewport. // * Scrollback: Clear the contents of the scrollback. // * All: Do both - clear the visible viewport and the scrollback, leaving // only the cursor row at the top of the viewport. // Arguments: // - clearType: The type of clear to perform. // Return Value: // - void ControlCore::ClearBuffer(Control::ClearBufferType clearType) { if (clearType == Control::ClearBufferType::Scrollback || clearType == Control::ClearBufferType::All) { _terminal->EraseInDisplay(::Microsoft::Console::VirtualTerminal::DispatchTypes::EraseType::Scrollback); } if (clearType == Control::ClearBufferType::Screen || clearType == Control::ClearBufferType::All) { // Send a signal to conpty to clear the buffer. if (auto conpty{ _connection.try_as() }) { // ConPTY will emit sequences to sync up our buffer with its new // contents. conpty.ClearBuffer(); } } } hstring ControlCore::ReadEntireBuffer() const { auto terminalLock = _terminal->LockForWriting(); const auto& textBuffer = _terminal->GetTextBuffer(); std::wstringstream ss; const auto lastRow = textBuffer.GetLastNonSpaceCharacter().Y; for (auto rowIndex = 0; rowIndex <= lastRow; rowIndex++) { const auto& row = textBuffer.GetRowByOffset(rowIndex); auto rowText = row.GetText(); const auto strEnd = rowText.find_last_not_of(UNICODE_SPACE); if (strEnd != std::string::npos) { rowText.erase(strEnd + 1); ss << rowText; } if (!row.WasWrapForced()) { ss << UNICODE_CARRIAGERETURN << UNICODE_LINEFEED; } } return hstring(ss.str()); } // Helper to check if we're on Windows 11 or not. This is used to check if // we need to use acrylic to achieve transparency, because vintage opacity // doesn't work in islands on win10. // Remove when we can remove the rest of GH#11285 bool ControlCore::IsVintageOpacityAvailable() noexcept { OSVERSIONINFOEXW osver{}; osver.dwOSVersionInfoSize = sizeof(osver); osver.dwBuildNumber = 22000; DWORDLONG dwlConditionMask = 0; VER_SET_CONDITION(dwlConditionMask, VER_BUILDNUMBER, VER_GREATER_EQUAL); return VerifyVersionInfoW(&osver, VER_BUILDNUMBER, dwlConditionMask) != FALSE; } }