// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "screenInfo.hpp" #include "dbcs.h" #include "output.h" #include "_output.h" #include "misc.h" #include "handle.h" #include "../buffer/out/CharRow.hpp" #include #include "../interactivity/inc/ServiceLocator.hpp" #include "../types/inc/Viewport.hpp" #include "../types/inc/GlyphWidth.hpp" #include "../terminal/parser/OutputStateMachineEngine.hpp" #include "../types/inc/convert.hpp" #pragma hdrstop using namespace Microsoft::Console; using namespace Microsoft::Console::Types; using namespace Microsoft::Console::Render; using namespace Microsoft::Console::Interactivity; using namespace Microsoft::Console::VirtualTerminal; #pragma region Construct_Destruct SCREEN_INFORMATION::SCREEN_INFORMATION( _In_ IWindowMetrics* pMetrics, _In_ IAccessibilityNotifier* pNotifier, const TextAttribute popupAttributes, const FontInfo fontInfo) : OutputMode{ ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT }, ResizingWindow{ 0 }, WheelDelta{ 0 }, HWheelDelta{ 0 }, _textBuffer{ nullptr }, Next{ nullptr }, WriteConsoleDbcsLeadByte{ 0, 0 }, FillOutDbcsLeadChar{ 0 }, ConvScreenInfo{ nullptr }, ScrollScale{ 1ul }, _pConsoleWindowMetrics{ pMetrics }, _pAccessibilityNotifier{ pNotifier }, _stateMachine{ nullptr }, _scrollMargins{ Viewport::FromCoord({ 0 }) }, _viewport(Viewport::Empty()), _psiAlternateBuffer{ nullptr }, _psiMainBuffer{ nullptr }, _rcAltSavedClientNew{ 0 }, _rcAltSavedClientOld{ 0 }, _fAltWindowChanged{ false }, _PopupAttributes{ popupAttributes }, _virtualBottom{ 0 }, _renderTarget{ *this }, _currentFont{ fontInfo }, _desiredFont{ fontInfo }, _ignoreLegacyEquivalentVTAttributes{ false } { // Check if VT mode is enabled. Note that this can be true w/o calling // SetConsoleMode, if VirtualTerminalLevel is set to !=0 in the registry. const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (gci.GetVirtTermLevel() != 0) { OutputMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; } } // Routine Description: // - This routine frees the memory associated with a screen buffer. // Arguments: // - ScreenInfo - screen buffer data to free. // Return Value: // Note: // - console handle table lock must be held when calling this routine SCREEN_INFORMATION::~SCREEN_INFORMATION() { _FreeOutputStateMachine(); } // Routine Description: // - This routine allocates and initializes the data associated with a screen buffer. // Arguments: // - ScreenInformation - the new screen buffer. // - coordWindowSize - the initial size of screen buffer's window (in rows/columns) // - nFont - the initial font to generate text with. // - dwScreenBufferSize - the initial size of the screen buffer (in rows/columns). // Return Value: [[nodiscard]] NTSTATUS SCREEN_INFORMATION::CreateInstance(_In_ COORD coordWindowSize, const FontInfo fontInfo, _In_ COORD coordScreenBufferSize, const TextAttribute defaultAttributes, const TextAttribute popupAttributes, const UINT uiCursorSize, _Outptr_ SCREEN_INFORMATION** const ppScreen) { *ppScreen = nullptr; try { IWindowMetrics* pMetrics = ServiceLocator::LocateWindowMetrics(); THROW_HR_IF_NULL(E_FAIL, pMetrics); IAccessibilityNotifier* pNotifier = ServiceLocator::LocateAccessibilityNotifier(); // It is possible for pNotifier to be null and that's OK. // For instance, the PTY doesn't need to send events. Just pass it along // and be sure that `SCREEN_INFORMATION` bypasses all event work if it's not there. SCREEN_INFORMATION* const pScreen = new SCREEN_INFORMATION(pMetrics, pNotifier, popupAttributes, fontInfo); // Set up viewport pScreen->_viewport = Viewport::FromDimensions({ 0, 0 }, pScreen->_IsInPtyMode() ? coordScreenBufferSize : coordWindowSize); pScreen->UpdateBottom(); // Set up text buffer pScreen->_textBuffer = std::make_unique(coordScreenBufferSize, defaultAttributes, uiCursorSize, pScreen->_renderTarget); const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); pScreen->_textBuffer->GetCursor().SetType(gci.GetCursorType()); const NTSTATUS status = pScreen->_InitializeOutputStateMachine(); if (NT_SUCCESS(status)) { *ppScreen = pScreen; } LOG_IF_NTSTATUS_FAILED(status); return status; } catch (...) { return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } } Viewport SCREEN_INFORMATION::GetBufferSize() const { return _textBuffer->GetSize(); } // Method Description: // - Returns the "terminal" dimensions of this buffer. If we're in Terminal // Scrolling mode, this will return our Y dimension as only extending up to // the _virtualBottom. The height of the returned viewport would then be // (number of lines in scrollback) + (number of lines in viewport). // If we're not in terminal scrolling mode, this will return our normal buffer // size. // Arguments: // - // Return Value: // - a viewport whose height is the height of the "terminal" portion of the // buffer in terminal scrolling mode, and is the height of the full buffer // in normal scrolling mode. Viewport SCREEN_INFORMATION::GetTerminalBufferSize() const { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Viewport v = _textBuffer->GetSize(); if (gci.IsTerminalScrolling() && v.Height() > _virtualBottom) { v = Viewport::FromDimensions({ 0, 0 }, v.Width(), _virtualBottom + 1); } return v; } const StateMachine& SCREEN_INFORMATION::GetStateMachine() const { return *_stateMachine; } StateMachine& SCREEN_INFORMATION::GetStateMachine() { return *_stateMachine; } // Routine Description: // - This routine inserts the screen buffer pointer into the console's list of screen buffers. // Arguments: // - ScreenInfo - Pointer to screen information structure. // Return Value: // Note: // - The console lock must be held when calling this routine. void SCREEN_INFORMATION::s_InsertScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo) { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); FAIL_FAST_IF(!(gci.IsConsoleLocked())); pScreenInfo->Next = gci.ScreenBuffers; gci.ScreenBuffers = pScreenInfo; } // Routine Description: // - This routine removes the screen buffer pointer from the console's list of screen buffers. // Arguments: // - ScreenInfo - Pointer to screen information structure. // Return Value: // Note: // - The console lock must be held when calling this routine. void SCREEN_INFORMATION::s_RemoveScreenBuffer(_In_ SCREEN_INFORMATION* const pScreenInfo) { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (pScreenInfo == gci.ScreenBuffers) { gci.ScreenBuffers = pScreenInfo->Next; } else { auto* Cur = gci.ScreenBuffers; auto* Prev = Cur; while (Cur != nullptr) { if (pScreenInfo == Cur) { break; } Prev = Cur; Cur = Cur->Next; } FAIL_FAST_IF_NULL(Cur); Prev->Next = Cur->Next; } if (pScreenInfo == gci.pCurrentScreenBuffer && gci.ScreenBuffers != gci.pCurrentScreenBuffer) { if (gci.ScreenBuffers != nullptr) { SetActiveScreenBuffer(*gci.ScreenBuffers); } else { gci.pCurrentScreenBuffer = nullptr; } } delete pScreenInfo; } #pragma endregion #pragma region Output State Machine [[nodiscard]] NTSTATUS SCREEN_INFORMATION::_InitializeOutputStateMachine() { try { auto getset = std::make_unique(*this); auto defaults = std::make_unique(*this); auto adapter = std::make_unique(std::move(getset), std::move(defaults)); auto engine = std::make_unique(std::move(adapter)); // Note that at this point in the setup, we haven't determined if we're // in VtIo mode or not yet. We'll set the OutputStateMachine's // TerminalConnection later, in VtIo::StartIfNeeded _stateMachine = std::make_shared(std::move(engine)); } catch (...) { // if any part of initialization failed, free the allocated ones. _FreeOutputStateMachine(); return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } return STATUS_SUCCESS; } // If we're an alternate buffer, we want to give the GetSet back to our main void SCREEN_INFORMATION::_FreeOutputStateMachine() { if (_psiMainBuffer == nullptr) // If this is a main buffer { if (_psiAlternateBuffer != nullptr) { s_RemoveScreenBuffer(_psiAlternateBuffer); } _stateMachine.reset(); } } #pragma endregion #pragma region IIoProvider // Method Description: // - Return the active screen buffer of the console. // Arguments: // - // Return Value: // - the active screen buffer of the console. SCREEN_INFORMATION& SCREEN_INFORMATION::GetActiveOutputBuffer() { return GetActiveBuffer(); } const SCREEN_INFORMATION& SCREEN_INFORMATION::GetActiveOutputBuffer() const { return GetActiveBuffer(); } // Method Description: // - Return the active input buffer of the console. // Arguments: // - // Return Value: // - the active input buffer of the console. InputBuffer* const SCREEN_INFORMATION::GetActiveInputBuffer() const { return ServiceLocator::LocateGlobals().getConsoleInformation().GetActiveInputBuffer(); } #pragma endregion #pragma region Get Data bool SCREEN_INFORMATION::IsActiveScreenBuffer() const { // the following macro returns TRUE if the given screen buffer is the active screen buffer. //#define ACTIVE_SCREEN_BUFFER(SCREEN_INFO) (gci.CurrentScreenBuffer == SCREEN_INFO) const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); return (gci.pCurrentScreenBuffer == this); } // Routine Description: // - This routine returns data about the screen buffer. // Arguments: // - Size - Pointer to location in which to store screen buffer size. // - CursorPosition - Pointer to location in which to store the cursor position. // - ScrollPosition - Pointer to location in which to store the scroll position. // - Attributes - Pointer to location in which to store the default attributes. // - CurrentWindowSize - Pointer to location in which to store current window size. // - MaximumWindowSize - Pointer to location in which to store maximum window size. // Return Value: // - None void SCREEN_INFORMATION::GetScreenBufferInformation(_Out_ PCOORD pcoordSize, _Out_ PCOORD pcoordCursorPosition, _Out_ PSMALL_RECT psrWindow, _Out_ PWORD pwAttributes, _Out_ PCOORD pcoordMaximumWindowSize, _Out_ PWORD pwPopupAttributes, _Out_writes_(COLOR_TABLE_SIZE) LPCOLORREF lpColorTable) const { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); *pcoordSize = GetBufferSize().Dimensions(); *pcoordCursorPosition = _textBuffer->GetCursor().GetPosition(); *psrWindow = _viewport.ToInclusive(); *pwAttributes = GetAttributes().GetLegacyAttributes(); *pwPopupAttributes = _PopupAttributes.GetLegacyAttributes(); // the copy length must be constant for now to keep OACR happy with buffer overruns. for (size_t i = 0; i < COLOR_TABLE_SIZE; i++) { lpColorTable[i] = gci.GetLegacyColorTableEntry(i); } *pcoordMaximumWindowSize = GetMaxWindowSizeInCharacters(); } // Routine Description: // - Gets the smallest possible client area in characters. Takes the window client area and divides by the active font dimensions. // Arguments: // - coordFontSize - The font size to use for calculation if a screen buffer is not yet attached. // Return Value: // - COORD containing the width and height representing the minimum character grid that can be rendered in the window. COORD SCREEN_INFORMATION::GetMinWindowSizeInCharacters(const COORD coordFontSize /*= { 1, 1 }*/) const { FAIL_FAST_IF(coordFontSize.X == 0); FAIL_FAST_IF(coordFontSize.Y == 0); // prepare rectangle RECT const rcWindowInPixels = _pConsoleWindowMetrics->GetMinClientRectInPixels(); // assign the pixel widths and heights to the final output COORD coordClientAreaSize; coordClientAreaSize.X = (SHORT)RECT_WIDTH(&rcWindowInPixels); coordClientAreaSize.Y = (SHORT)RECT_HEIGHT(&rcWindowInPixels); // now retrieve the font size and divide the pixel counts into character counts COORD coordFont = coordFontSize; // by default, use the size we were given // If text info has been set up, instead retrieve its font size if (_textBuffer != nullptr) { coordFont = GetScreenFontSize(); } FAIL_FAST_IF(coordFont.X == 0); FAIL_FAST_IF(coordFont.Y == 0); coordClientAreaSize.X /= coordFont.X; coordClientAreaSize.Y /= coordFont.Y; return coordClientAreaSize; } // Routine Description: // - Gets the maximum client area in characters that would fit on the current monitor or given the current buffer size. // Takes the monitor work area and divides by the active font dimensions then limits by buffer size. // Arguments: // - coordFontSize - The font size to use for calculation if a screen buffer is not yet attached. // Return Value: // - COORD containing the width and height representing the largest character // grid that can be rendered on the current monitor and/or from the current buffer size. COORD SCREEN_INFORMATION::GetMaxWindowSizeInCharacters(const COORD coordFontSize /*= { 1, 1 }*/) const { FAIL_FAST_IF(coordFontSize.X == 0); FAIL_FAST_IF(coordFontSize.Y == 0); const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); COORD coordClientAreaSize = coordScreenBufferSize; // Important re: headless consoles on onecore (for telnetd, etc.) // GetConsoleScreenBufferInfoEx hits this to get the max size of the display. // Because we're headless, we don't really care about the max size of the display. // In that case, we'll just return the buffer size as the "max" window size. if (!ServiceLocator::LocateGlobals().IsHeadless()) { const COORD coordWindowRestrictedSize = GetLargestWindowSizeInCharacters(coordFontSize); // If the buffer is smaller than what the max window would allow, then the max client area can only be as big as the // buffer we have. coordClientAreaSize.X = std::min(coordScreenBufferSize.X, coordWindowRestrictedSize.X); coordClientAreaSize.Y = std::min(coordScreenBufferSize.Y, coordWindowRestrictedSize.Y); } return coordClientAreaSize; } // Routine Description: // - Gets the largest possible client area in characters if the window were stretched as large as it could go. // - Takes the window client area and divides by the active font dimensions. // Arguments: // - coordFontSize - The font size to use for calculation if a screen buffer is not yet attached. // Return Value: // - COORD containing the width and height representing the largest character // grid that can be rendered on the current monitor with the maximum size window. COORD SCREEN_INFORMATION::GetLargestWindowSizeInCharacters(const COORD coordFontSize /*= { 1, 1 }*/) const { FAIL_FAST_IF(coordFontSize.X == 0); FAIL_FAST_IF(coordFontSize.Y == 0); RECT const rcClientInPixels = _pConsoleWindowMetrics->GetMaxClientRectInPixels(); // first assign the pixel widths and heights to the final output COORD coordClientAreaSize; coordClientAreaSize.X = (SHORT)RECT_WIDTH(&rcClientInPixels); coordClientAreaSize.Y = (SHORT)RECT_HEIGHT(&rcClientInPixels); // now retrieve the font size and divide the pixel counts into character counts COORD coordFont = coordFontSize; // by default, use the size we were given // If renderer has been set up, instead retrieve its font size if (ServiceLocator::LocateGlobals().pRender != nullptr) { coordFont = GetScreenFontSize(); } FAIL_FAST_IF(coordFont.X == 0); FAIL_FAST_IF(coordFont.Y == 0); coordClientAreaSize.X /= coordFont.X; coordClientAreaSize.Y /= coordFont.Y; return coordClientAreaSize; } COORD SCREEN_INFORMATION::GetScrollBarSizesInCharacters() const { COORD coordFont = GetScreenFontSize(); SHORT vScrollSize = ServiceLocator::LocateGlobals().sVerticalScrollSize; SHORT hScrollSize = ServiceLocator::LocateGlobals().sHorizontalScrollSize; COORD coordBarSizes; coordBarSizes.X = (vScrollSize / coordFont.X) + ((vScrollSize % coordFont.X) != 0 ? 1 : 0); coordBarSizes.Y = (hScrollSize / coordFont.Y) + ((hScrollSize % coordFont.Y) != 0 ? 1 : 0); return coordBarSizes; } void SCREEN_INFORMATION::GetRequiredConsoleSizeInPixels(_Out_ PSIZE const pRequiredSize) const { COORD const coordFontSize = GetCurrentFont().GetSize(); // TODO: Assert valid size boundaries pRequiredSize->cx = GetViewport().Width() * coordFontSize.X; pRequiredSize->cy = GetViewport().Height() * coordFontSize.Y; } COORD SCREEN_INFORMATION::GetScreenFontSize() const { // If we have no renderer, then we don't really need any sort of pixel math. so the "font size" for the scale factor // (which is used almost everywhere around the code as * and / calls) should just be 1,1 so those operations will do // effectively nothing. COORD coordRet = { 1, 1 }; if (ServiceLocator::LocateGlobals().pRender != nullptr) { coordRet = GetCurrentFont().GetSize(); } // For sanity's sake, make sure not to leak 0 out as a possible value. These values are used in division operations. coordRet.X = std::max(coordRet.X, 1i16); coordRet.Y = std::max(coordRet.Y, 1i16); return coordRet; } #pragma endregion #pragma region Set Data void SCREEN_INFORMATION::RefreshFontWithRenderer() { if (IsActiveScreenBuffer()) { // Hand the handle to our internal structure to the font change trigger in case it updates it based on what's appropriate. if (ServiceLocator::LocateGlobals().pRender != nullptr) { ServiceLocator::LocateGlobals().pRender->TriggerFontChange(ServiceLocator::LocateGlobals().dpi, GetDesiredFont(), GetCurrentFont()); NotifyGlyphWidthFontChanged(); } } } void SCREEN_INFORMATION::UpdateFont(const FontInfo* const pfiNewFont) { FontInfoDesired fiDesiredFont(*pfiNewFont); GetDesiredFont() = fiDesiredFont; RefreshFontWithRenderer(); // If we're the active screen buffer... if (IsActiveScreenBuffer()) { // If there is a window attached, let it know that it should try to update so the rows/columns are now accounting for the new font. IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); if (nullptr != pWindow) { COORD coordViewport = GetViewport().Dimensions(); pWindow->UpdateWindowSize(coordViewport); } } // If we're an alt buffer, also update our main buffer. if (_psiMainBuffer) { _psiMainBuffer->UpdateFont(pfiNewFont); } } // Routine Description: // - Informs clients whether we have accessibility eventing so they can // save themselves the work of performing math or lookups before calling // `NotifyAccessibilityEventing`. // Arguments: // - // Return Value: // - True if we have an accessibility listener. False otherwise. bool SCREEN_INFORMATION::HasAccessibilityEventing() const noexcept { return _pAccessibilityNotifier; } // NOTE: This method was historically used to notify accessibility apps AND // to aggregate drawing metadata to determine whether or not to use PolyTextOut. // After the Nov 2015 graphics refactor, the metadata drawing flag calculation is no longer necessary. // This now only notifies accessibility apps of a change. void SCREEN_INFORMATION::NotifyAccessibilityEventing(const short sStartX, const short sStartY, const short sEndX, const short sEndY) { if (!_pAccessibilityNotifier) { return; } // Fire off a winevent to let accessibility apps know what changed. if (IsActiveScreenBuffer()) { const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); FAIL_FAST_IF(!(sEndX < coordScreenBufferSize.X)); if (sStartX == sEndX && sStartY == sEndY) { try { const auto cellData = GetCellDataAt({ sStartX, sStartY }); const LONG charAndAttr = MAKELONG(Utf16ToUcs2(cellData->Chars()), cellData->TextAttr().GetLegacyAttributes()); _pAccessibilityNotifier->NotifyConsoleUpdateSimpleEvent(MAKELONG(sStartX, sStartY), charAndAttr); } catch (...) { LOG_HR(wil::ResultFromCaughtException()); return; } } else { _pAccessibilityNotifier->NotifyConsoleUpdateRegionEvent(MAKELONG(sStartX, sStartY), MAKELONG(sEndX, sEndY)); } IConsoleWindow* pConsoleWindow = ServiceLocator::LocateConsoleWindow(); if (pConsoleWindow) { LOG_IF_FAILED(pConsoleWindow->SignalUia(UIA_Text_TextChangedEventId)); // TODO MSFT 7960168 do we really need this event to not signal? //pConsoleWindow->SignalUia(UIA_LayoutInvalidatedEventId); } } } #pragma endregion #pragma region UI_Refresh VOID SCREEN_INFORMATION::UpdateScrollBars() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (!IsActiveScreenBuffer()) { return; } if (gci.Flags & CONSOLE_UPDATING_SCROLL_BARS) { return; } gci.Flags |= CONSOLE_UPDATING_SCROLL_BARS; if (ServiceLocator::LocateConsoleWindow() != nullptr) { ServiceLocator::LocateConsoleWindow()->PostUpdateScrollBars(); } } VOID SCREEN_INFORMATION::InternalUpdateScrollBars() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); IConsoleWindow* const pWindow = ServiceLocator::LocateConsoleWindow(); WI_ClearFlag(gci.Flags, CONSOLE_UPDATING_SCROLL_BARS); if (!IsActiveScreenBuffer()) { return; } ResizingWindow++; if (pWindow != nullptr) { const auto buffer = GetBufferSize(); // If this is the main buffer, make sure we enable both of the scroll bars. // The alt buffer likely disabled the scroll bars, this is the only // way to re-enable it. if (!_IsAltBuffer()) { pWindow->EnableBothScrollBars(); } pWindow->UpdateScrollBar(true, _IsAltBuffer() || gci.IsTerminalScrolling(), _viewport.Height(), gci.IsTerminalScrolling() ? _virtualBottom : buffer.BottomInclusive(), _viewport.Top()); pWindow->UpdateScrollBar(false, _IsAltBuffer(), _viewport.Width(), buffer.RightInclusive(), _viewport.Left()); } // Fire off an event to let accessibility apps know the layout has changed. if (_pAccessibilityNotifier) { _pAccessibilityNotifier->NotifyConsoleLayoutEvent(); } ResizingWindow--; } // Routine Description: // - Modifies the size of the current viewport to match the width/height of the request given. // - This will act like a resize operation from the bottom right corner of the window. // Arguments: // - pcoordSize - Requested viewport width/heights in characters // Return Value: // - void SCREEN_INFORMATION::SetViewportSize(const COORD* const pcoordSize) { // If this is the alt buffer or a VT I/O buffer: // first resize ourselves to match the new viewport // then also make sure that the main buffer gets the same call // (if necessary) if (_IsInPtyMode()) { LOG_IF_FAILED(ResizeScreenBuffer(*pcoordSize, TRUE)); if (_psiMainBuffer) { const auto bufferSize = GetBufferSize().Dimensions(); _psiMainBuffer->SetViewportSize(&bufferSize); } } _InternalSetViewportSize(pcoordSize, false, false); } // Method Description: // - Update the origin of the buffer's viewport. You can either move the // viewport with a delta relative to its current location, or set its // absolute origin. Either way leaves the dimensions of the viewport // unchanged. Also potentially updates our "virtual bottom", the last real // location of the viewport in the buffer. // Also notifies the window implementation to update its scrollbars. // Arguments: // - fAbsolute: If true, coordWindowOrigin is the absolute location of the origin of the new viewport. // If false, coordWindowOrigin is a delta to move the viewport relative to its current position. // - coordWindowOrigin: Either the new absolute position of the origin of the // viewport, or a delta to add to the current viewport location. // - updateBottom: If true, update our virtual bottom position. This should be // false if we're moving the viewport in response to the users scrolling up // and down in the buffer, but API calls should set this to true. // Return Value: // - STATUS_INVALID_PARAMETER if the new viewport would be outside the buffer, // else STATUS_SUCCESS [[nodiscard]] NTSTATUS SCREEN_INFORMATION::SetViewportOrigin(const bool fAbsolute, const COORD coordWindowOrigin, const bool updateBottom) { // calculate window size COORD WindowSize = _viewport.Dimensions(); SMALL_RECT NewWindow; // if relative coordinates, figure out absolute coords. if (!fAbsolute) { if (coordWindowOrigin.X == 0 && coordWindowOrigin.Y == 0) { return STATUS_SUCCESS; } NewWindow.Left = _viewport.Left() + coordWindowOrigin.X; NewWindow.Top = _viewport.Top() + coordWindowOrigin.Y; } else { if (coordWindowOrigin == _viewport.Origin()) { return STATUS_SUCCESS; } NewWindow.Left = coordWindowOrigin.X; NewWindow.Top = coordWindowOrigin.Y; } NewWindow.Right = (SHORT)(NewWindow.Left + WindowSize.X - 1); NewWindow.Bottom = (SHORT)(NewWindow.Top + WindowSize.Y - 1); const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); // If we're in terminal scrolling mode, and we're trying to set the viewport // below the logical viewport, without updating our virtual bottom // (the logical viewport's position), dont. // Instead move us to the bottom of the logical viewport. if (gci.IsTerminalScrolling() && !updateBottom && NewWindow.Bottom > _virtualBottom) { const short delta = _virtualBottom - NewWindow.Bottom; NewWindow.Top += delta; NewWindow.Bottom += delta; } // see if new window origin would extend window beyond extent of screen buffer const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); if (NewWindow.Left < 0 || NewWindow.Top < 0 || NewWindow.Right < 0 || NewWindow.Bottom < 0 || NewWindow.Right >= coordScreenBufferSize.X || NewWindow.Bottom >= coordScreenBufferSize.Y) { return STATUS_INVALID_PARAMETER; } if (IsActiveScreenBuffer() && ServiceLocator::LocateConsoleWindow() != nullptr) { // Tell the window that it needs to set itself to the new origin if we're the active buffer. ServiceLocator::LocateConsoleWindow()->ChangeViewport(NewWindow); } else { // Otherwise, just store the new position and go on. _viewport = Viewport::FromInclusive(NewWindow); Tracing::s_TraceWindowViewport(_viewport); } // Update our internal virtual bottom tracker if requested. This helps keep // the viewport's logical position consistent from the perspective of a // VT client application, even if the user scrolls the viewport with the mouse. if (updateBottom) { UpdateBottom(); } return STATUS_SUCCESS; } bool SCREEN_INFORMATION::SendNotifyBeep() const { if (IsActiveScreenBuffer()) { if (ServiceLocator::LocateConsoleWindow() != nullptr) { return ServiceLocator::LocateConsoleWindow()->SendNotifyBeep(); } } return false; } bool SCREEN_INFORMATION::PostUpdateWindowSize() const { if (IsActiveScreenBuffer()) { if (ServiceLocator::LocateConsoleWindow() != nullptr) { return ServiceLocator::LocateConsoleWindow()->PostUpdateWindowSize(); } } return false; } // Routine Description: // - Modifies the screen buffer and viewport dimensions when the available client area rendering space changes. // Arguments: // - prcClientNew - Client rectangle in pixels after this update // - prcClientOld - Client rectangle in pixels before this update // Return Value: // - void SCREEN_INFORMATION::ProcessResizeWindow(const RECT* const prcClientNew, const RECT* const prcClientOld) { if (_IsAltBuffer()) { // Stash away the size of the window, we'll need to do this to the main when we pop back // We set this on the main, so that main->alt(resize)->alt keeps the resize _psiMainBuffer->_fAltWindowChanged = true; _psiMainBuffer->_rcAltSavedClientNew = *prcClientNew; _psiMainBuffer->_rcAltSavedClientOld = *prcClientOld; } // 1.a In some modes, the screen buffer size needs to change on window size, // so do that first. // _AdjustScreenBuffer might hide the commandline. If it does so, it'll // return S_OK instead of S_FALSE. In that case, we'll need to re-show // the commandline ourselves once the viewport size is updated. // (See 1.b below) const HRESULT adjustBufferSizeResult = _AdjustScreenBuffer(prcClientNew); LOG_IF_FAILED(adjustBufferSizeResult); // 2. Now calculate how large the new viewport should be COORD coordViewportSize; _CalculateViewportSize(prcClientNew, &coordViewportSize); // 3. And adjust the existing viewport to match the same dimensions. // The old/new comparison is to figure out which side the window was resized from. _AdjustViewportSize(prcClientNew, prcClientOld, &coordViewportSize); // 1.b If we did actually change the buffer size, then we need to show the // commandline again. We hid it during _AdjustScreenBuffer, but we // couldn't turn it back on until the Viewport was updated to the new // size. See MSFT:19976291 if (SUCCEEDED(adjustBufferSizeResult) && adjustBufferSizeResult != S_FALSE) { CommandLine& commandLine = CommandLine::Instance(); commandLine.Show(); } // 4. Finally, update the scroll bars. UpdateScrollBars(); FAIL_FAST_IF(!(_viewport.Top() >= 0)); // TODO MSFT: 17663344 - Audit call sites for this precondition. Extremely tiny offscreen windows. //FAIL_FAST_IF(!(_viewport.IsValid())); } #pragma endregion #pragma region Support_Calculation // Routine Description: // - This helper converts client pixel areas into the number of characters that could fit into the client window. // - It requires the buffer size to figure out whether it needs to reserve space for the scroll bars (or not). // Arguments: // - prcClientNew - Client region of window in pixels // - coordBufferOld - Size of backing buffer in characters // - pcoordClientNewCharacters - The maximum number of characters X by Y that can be displayed in the window with the given backing buffer. // Return Value: // - S_OK if math was successful. Check with SUCCEEDED/FAILED macro. [[nodiscard]] HRESULT SCREEN_INFORMATION::_AdjustScreenBufferHelper(const RECT* const prcClientNew, const COORD coordBufferOld, _Out_ COORD* const pcoordClientNewCharacters) { // Get the font size ready. COORD const coordFontSize = GetScreenFontSize(); // We cannot operate if the font size is 0. This shouldn't happen, but stop early if it does. RETURN_HR_IF(E_NOT_VALID_STATE, 0 == coordFontSize.X || 0 == coordFontSize.Y); // Find out how much client space we have to work with in the new area. SIZE sizeClientNewPixels = { 0 }; sizeClientNewPixels.cx = RECT_WIDTH(prcClientNew); sizeClientNewPixels.cy = RECT_HEIGHT(prcClientNew); // Subtract out scroll bar space if scroll bars will be necessary. bool fIsHorizontalVisible = false; bool fIsVerticalVisible = false; s_CalculateScrollbarVisibility(prcClientNew, &coordBufferOld, &coordFontSize, &fIsHorizontalVisible, &fIsVerticalVisible); if (fIsHorizontalVisible) { sizeClientNewPixels.cy -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; } if (fIsVerticalVisible) { sizeClientNewPixels.cx -= ServiceLocator::LocateGlobals().sVerticalScrollSize; } // Now with the scroll bars removed, calculate how many characters could fit into the new window area. pcoordClientNewCharacters->X = (SHORT)(sizeClientNewPixels.cx / coordFontSize.X); pcoordClientNewCharacters->Y = (SHORT)(sizeClientNewPixels.cy / coordFontSize.Y); // If the new client is too tiny, our viewport will be 1x1. pcoordClientNewCharacters->X = std::max(pcoordClientNewCharacters->X, 1i16); pcoordClientNewCharacters->Y = std::max(pcoordClientNewCharacters->Y, 1i16); return S_OK; } // Routine Description: // - Modifies the size of the backing text buffer when the window changes to support "intuitive" resizing modes by grabbing the window edges. // - This function will compensate for scroll bars. // - Buffer size changes will happen internally to this function. // Arguments: // - prcClientNew - Client rectangle in pixels after this update // Return Value: // - appropriate HRESULT [[nodiscard]] HRESULT SCREEN_INFORMATION::_AdjustScreenBuffer(const RECT* const prcClientNew) { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); // Prepare the buffer sizes. // We need the main's size here to maintain the right scrollbar visibility. COORD const coordBufferSizeOld = _IsAltBuffer() ? _psiMainBuffer->GetBufferSize().Dimensions() : GetBufferSize().Dimensions(); COORD coordBufferSizeNew = coordBufferSizeOld; // First figure out how many characters we could fit into the new window given the old buffer size COORD coordClientNewCharacters; RETURN_IF_FAILED(_AdjustScreenBufferHelper(prcClientNew, coordBufferSizeOld, &coordClientNewCharacters)); // If we're in wrap text mode, then we want to be fixed to the window size. So use the character calculation we just got // to fix the buffer and window width together. if (gci.GetWrapText()) { coordBufferSizeNew.X = coordClientNewCharacters.X; } // Reanalyze scroll bars in case we fixed the edge together for word wrap. // Use the new buffer client size. RETURN_IF_FAILED(_AdjustScreenBufferHelper(prcClientNew, coordBufferSizeNew, &coordClientNewCharacters)); // Now reanalyze the buffer size and grow if we can fit more characters into the window no matter the console mode. if (_IsInPtyMode()) { // The alt buffer always wants to be exactly the size of the screen, never more or less. // This prevents scrollbars when you increase the alt buffer size, then decrease it. // Can't have a buffer dimension of 0 - that'll cause divide by zeros in the future. coordBufferSizeNew.X = std::max(coordClientNewCharacters.X, 1i16); coordBufferSizeNew.Y = std::max(coordClientNewCharacters.Y, 1i16); } else { if (coordClientNewCharacters.X > coordBufferSizeNew.X) { coordBufferSizeNew.X = coordClientNewCharacters.X; } if (coordClientNewCharacters.Y > coordBufferSizeNew.Y) { coordBufferSizeNew.Y = coordClientNewCharacters.Y; } } HRESULT hr = S_FALSE; // Only attempt to modify the buffer if something changed. Expensive operation. if (coordBufferSizeOld.X != coordBufferSizeNew.X || coordBufferSizeOld.Y != coordBufferSizeNew.Y) { CommandLine& commandLine = CommandLine::Instance(); // TODO: Deleting and redrawing the command line during resizing can cause flickering. See: http://osgvsowi/658439 // 1. Delete input string if necessary (see menu.c) commandLine.Hide(FALSE); _textBuffer->GetCursor().SetIsVisible(false); // 2. Call the resize screen buffer method (expensive) to redimension the backing buffer (and reflow) LOG_IF_FAILED(ResizeScreenBuffer(coordBufferSizeNew, FALSE)); // MSFT:19976291 Don't re-show the commandline here. We need to wait for // the viewport to also get resized before we can re-show the commandline. // ProcessResizeWindow will call commandline.Show() for us. _textBuffer->GetCursor().SetIsVisible(true); // Return S_OK, to indicate we succeeded and actually did something. hr = S_OK; } return hr; } // Routine Description: // - Calculates what width/height the viewport must have to consume all the available space in the given client area. // - This compensates for scroll bars and will leave space in the client area for the bars if necessary. // Arguments: // - prcClientArea - The client rectangle in pixels of available rendering space. // - pcoordSize - Filled with the width/height to which the viewport should be set. // Return Value: // - void SCREEN_INFORMATION::_CalculateViewportSize(const RECT* const prcClientArea, _Out_ COORD* const pcoordSize) { COORD const coordBufferSize = GetBufferSize().Dimensions(); COORD const coordFontSize = GetScreenFontSize(); SIZE sizeClientPixels = { 0 }; sizeClientPixels.cx = RECT_WIDTH(prcClientArea); sizeClientPixels.cy = RECT_HEIGHT(prcClientArea); bool fIsHorizontalVisible; bool fIsVerticalVisible; s_CalculateScrollbarVisibility(prcClientArea, &coordBufferSize, &coordFontSize, &fIsHorizontalVisible, &fIsVerticalVisible); if (fIsHorizontalVisible) { sizeClientPixels.cy -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; } if (fIsVerticalVisible) { sizeClientPixels.cx -= ServiceLocator::LocateGlobals().sVerticalScrollSize; } pcoordSize->X = (SHORT)(sizeClientPixels.cx / coordFontSize.X); pcoordSize->Y = (SHORT)(sizeClientPixels.cy / coordFontSize.Y); } // Routine Description: // - Modifies the size of the current viewport to match the width/height of the request given. // - Must specify which corner to adjust from. Default to false/false to resize from the bottom right corner. // Arguments: // - pcoordSize - Requested viewport width/heights in characters // - fResizeFromTop - If false, will trim/add to bottom of viewport first. If true, will trim/add to top. // - fResizeFromBottom - If false, will trim/add to top of viewport first. If true, will trim/add to left. // Return Value: // - void SCREEN_INFORMATION::_InternalSetViewportSize(const COORD* const pcoordSize, const bool fResizeFromTop, const bool fResizeFromLeft) { const short DeltaX = pcoordSize->X - _viewport.Width(); const short DeltaY = pcoordSize->Y - _viewport.Height(); const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); // do adjustments on a copy that's easily manipulated. SMALL_RECT srNewViewport = _viewport.ToInclusive(); // Now we need to determine what our new Window size should // be. Note that Window here refers to the character/row window. if (fResizeFromLeft) { // we're being horizontally sized from the left border const SHORT sLeftProposed = (srNewViewport.Left - DeltaX); if (sLeftProposed >= 0) { // there's enough room in the backlog to just expand left srNewViewport.Left -= DeltaX; } else { // if we're resizing horizontally, we want to show as much // content above as we can, but we can't show more // than the left of the window srNewViewport.Left = 0; srNewViewport.Right += (SHORT)abs(sLeftProposed); } } else { // we're being horizontally sized from the right border const SHORT sRightProposed = (srNewViewport.Right + DeltaX); if (sRightProposed <= (coordScreenBufferSize.X - 1)) { srNewViewport.Right += DeltaX; } else { srNewViewport.Right = (coordScreenBufferSize.X - 1); srNewViewport.Left -= (sRightProposed - (coordScreenBufferSize.X - 1)); } } if (fResizeFromTop) { const SHORT sTopProposed = (srNewViewport.Top - DeltaY); // we're being vertically sized from the top border if (sTopProposed >= 0) { // Special case: Only modify the top position if we're not // on the 0th row of the buffer. // If we're on the 0th row, people expect it to stay stuck // to the top of the window, not to start collapsing down // and hiding the top rows. if (srNewViewport.Top > 0) { // there's enough room in the backlog to just expand the top srNewViewport.Top -= DeltaY; } else { // If we didn't adjust the top, we need to trim off // the number of rows from the bottom instead. // NOTE: It's += because DeltaY will be negative // already for this circumstance. FAIL_FAST_IF(!(DeltaY <= 0)); srNewViewport.Bottom += DeltaY; } } else { // if we're resizing vertically, we want to show as much // content above as we can, but we can't show more // than the top of the window srNewViewport.Top = 0; srNewViewport.Bottom += (SHORT)abs(sTopProposed); } } else { // we're being vertically sized from the bottom border const SHORT sBottomProposed = (srNewViewport.Bottom + DeltaY); if (sBottomProposed <= (coordScreenBufferSize.Y - 1)) { // If the new bottom is supposed to be before the final line of the buffer // Check to ensure that we don't hide the prompt by collapsing the window. // The final valid end position will be the coordinates of // the last character displayed (including any characters // in the input line) COORD coordValidEnd; Selection::Instance().GetValidAreaBoundaries(nullptr, &coordValidEnd); // If the bottom of the window when adjusted would be // above the final line of valid text... if (srNewViewport.Bottom + DeltaY < coordValidEnd.Y) { // Adjust the top of the window instead of the bottom // (so the lines slide upward) srNewViewport.Top -= DeltaY; // If we happened to move the top of the window past // the 0th row (first row in the buffer) if (srNewViewport.Top < 0) { // Find the amount we went past 0, correct the top // of the window back to 0, and instead adjust the // bottom even though it will cause us to lose the // prompt line. const short cRemainder = 0 - srNewViewport.Top; srNewViewport.Top += cRemainder; FAIL_FAST_IF(!(srNewViewport.Top == 0)); srNewViewport.Bottom += cRemainder; } } else { srNewViewport.Bottom += DeltaY; } } else { srNewViewport.Bottom = (coordScreenBufferSize.Y - 1); srNewViewport.Top -= (sBottomProposed - (coordScreenBufferSize.Y - 1)); } } // Ensure the viewport is valid. // We can't have a negative left or top. if (srNewViewport.Left < 0) { srNewViewport.Right -= srNewViewport.Left; srNewViewport.Left = 0; } if (srNewViewport.Top < 0) { srNewViewport.Bottom -= srNewViewport.Top; srNewViewport.Top = 0; } // Bottom and right cannot pass the final characters in the array. const SHORT offRightDelta = srNewViewport.Right - (coordScreenBufferSize.X - 1); if (offRightDelta > 0) // the viewport was off the right of the buffer... { // ...so slide both left/right back into the buffer. This will prevent us // from having a negative width later. srNewViewport.Right -= offRightDelta; srNewViewport.Left = std::max(0, srNewViewport.Left - offRightDelta); } srNewViewport.Bottom = std::min(srNewViewport.Bottom, gsl::narrow(coordScreenBufferSize.Y - 1)); // See MSFT:19917443 // If we're in terminal scrolling mode, and we've changed the height of the // viewport, the new viewport's bottom to the _virtualBottom. // GH#1206 - Only do this if the viewport is _growing_ in height. This can // cause unexpected behavior if we try to anchor the _virtualBottom to a // position that will be greater than the height of the buffer. const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto newViewport = Viewport::FromInclusive(srNewViewport); if (gci.IsTerminalScrolling() && newViewport.Height() >= _viewport.Height()) { const short newTop = static_cast(std::max(0, _virtualBottom - (newViewport.Height() - 1))); newViewport = Viewport::FromDimensions(COORD({ newViewport.Left(), newTop }), newViewport.Dimensions()); } _viewport = newViewport; UpdateBottom(); Tracing::s_TraceWindowViewport(_viewport); // In Conpty mode, call TriggerScroll here without params. By not providing // params, the renderer will make sure to update the VtEngine with the // updated viewport size. If we don't do this, the engine can get into a // torn state on this frame. // // Without this statement, the engine won't be told about the new view size // till the start of the next frame. If any other text gets output before // that frame starts, there's a very real chance that it'll cause errors as // the engine tries to invalidate those regions. if (gci.IsInVtIoMode() && ServiceLocator::LocateGlobals().pRender) { ServiceLocator::LocateGlobals().pRender->TriggerScroll(); } } // Routine Description: // - Modifies the size of the current viewport to match the width/height of the request given. // - Uses the old and new client areas to determine which side the window was resized from. // Arguments: // - prcClientNew - Client rectangle in pixels after this update // - prcClientOld - Client rectangle in pixels before this update // - pcoordSize - Requested viewport width/heights in characters // Return Value: // - void SCREEN_INFORMATION::_AdjustViewportSize(const RECT* const prcClientNew, const RECT* const prcClientOld, const COORD* const pcoordSize) { // If the left is the only one that changed (and not the right // also), then adjust from the left. Otherwise if the right // changes or both changed, bias toward leaving the top-left // corner in place and resize from the bottom right. // -- // Resizing from the bottom right is more expected by // users. Normally only one dimension (or one corner) will change // at a time if the user is moving it. However, if the window is // being dragged and forced to resize at a monitor boundary, all 4 // will change. In this case especially, users expect the top left // to stay in place and the bottom right to adapt. bool const fResizeFromLeft = prcClientNew->left != prcClientOld->left && prcClientNew->right == prcClientOld->right; bool const fResizeFromTop = prcClientNew->top != prcClientOld->top && prcClientNew->bottom == prcClientOld->bottom; const Viewport oldViewport = Viewport(_viewport); _InternalSetViewportSize(pcoordSize, fResizeFromTop, fResizeFromLeft); // MSFT 13194969, related to 12092729. // If we're in virtual terminal mode, and the viewport dimensions change, // send a WindowBufferSizeEvent. If the client wants VT mode, then they // probably want the viewport resizes, not just the screen buffer // resizes. This does change the behavior of the API for v2 callers, // but only callers who've requested VT mode. In 12092729, we enabled // sending notifications from window resizes in cases where the buffer // didn't resize, so this applies the same expansion to resizes using // the window, not the API. if (IsInVirtualTerminalInputMode()) { if ((_viewport.Width() != oldViewport.Width()) || (_viewport.Height() != oldViewport.Height())) { ScreenBufferSizeChange(GetBufferSize().Dimensions()); } } } // Routine Description: // - From a window client area in pixels, a buffer size, and the font size, this will determine // whether scroll bars will need to be shown (and consume a portion of the client area) for the // given buffer to be rendered. // Arguments: // - prcClientArea - Client area in pixels of the available space for rendering // - pcoordBufferSize - Buffer size in characters // - pcoordFontSize - Font size in pixels per character // - pfIsHorizontalVisible - Indicates whether the horizontal scroll // bar (consuming vertical space) will need to be visible // - pfIsVerticalVisible - Indicates whether the vertical scroll bar // (consuming horizontal space) will need to be visible // Return Value: // - void SCREEN_INFORMATION::s_CalculateScrollbarVisibility(const RECT* const prcClientArea, const COORD* const pcoordBufferSize, const COORD* const pcoordFontSize, _Out_ bool* const pfIsHorizontalVisible, _Out_ bool* const pfIsVerticalVisible) { // Start with bars not visible as the initial state of the client area doesn't account for scroll bars. *pfIsHorizontalVisible = false; *pfIsVerticalVisible = false; // Set up the client area in pixels SIZE sizeClientPixels = { 0 }; sizeClientPixels.cx = RECT_WIDTH(prcClientArea); sizeClientPixels.cy = RECT_HEIGHT(prcClientArea); // Set up the buffer area in pixels by multiplying the size by the font size scale factor SIZE sizeBufferPixels = { 0 }; sizeBufferPixels.cx = pcoordBufferSize->X * pcoordFontSize->X; sizeBufferPixels.cy = pcoordBufferSize->Y * pcoordFontSize->Y; // Now figure out whether we need one or both scroll bars. // Showing a scroll bar in one direction may necessitate showing // the scroll bar in the other (as it will consume client area // space). if (sizeBufferPixels.cx > sizeClientPixels.cx) { *pfIsHorizontalVisible = true; // If we have a horizontal bar, remove it from available // vertical space and check that remaining client area is // enough. sizeClientPixels.cy -= ServiceLocator::LocateGlobals().sHorizontalScrollSize; if (sizeBufferPixels.cy > sizeClientPixels.cy) { *pfIsVerticalVisible = true; } } else if (sizeBufferPixels.cy > sizeClientPixels.cy) { *pfIsVerticalVisible = true; // If we have a vertical bar, remove it from available // horizontal space and check that remaining client area is // enough. sizeClientPixels.cx -= ServiceLocator::LocateGlobals().sVerticalScrollSize; if (sizeBufferPixels.cx > sizeClientPixels.cx) { *pfIsHorizontalVisible = true; } } } bool SCREEN_INFORMATION::IsMaximizedBoth() const { return IsMaximizedX() && IsMaximizedY(); } bool SCREEN_INFORMATION::IsMaximizedX() const { // If the viewport is displaying the entire size of the allocated buffer, it's maximized. return _viewport.Left() == 0 && (_viewport.Width() == GetBufferSize().Width()); } bool SCREEN_INFORMATION::IsMaximizedY() const { // If the viewport is displaying the entire size of the allocated buffer, it's maximized. return _viewport.Top() == 0 && (_viewport.Height() == GetBufferSize().Height()); } #pragma endregion // Routine Description: // - This is a screen resize algorithm which will reflow the ends of lines based on the // line wrap state used for clipboard line-based copy. // Arguments: // - Coordinates of the new screen size // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeWithReflow(const COORD coordNewScreenSize) { if ((USHORT)coordNewScreenSize.X >= SHORT_MAX || (USHORT)coordNewScreenSize.Y >= SHORT_MAX) { RIPMSG2(RIP_WARNING, "Invalid screen buffer size (0x%x, 0x%x)", coordNewScreenSize.X, coordNewScreenSize.Y); return STATUS_INVALID_PARAMETER; } // First allocate a new text buffer to take the place of the current one. std::unique_ptr newTextBuffer; // GH#3848 - Stash away the current attributes the old text buffer is using. // We'll initialize the new buffer with the default attributes, but after // the resize, we'll want to make sure that the new buffer's current // attributes (the ones used for printing new text) match the old buffer's. const auto oldPrimaryAttributes = _textBuffer->GetCurrentAttributes(); try { newTextBuffer = std::make_unique(coordNewScreenSize, TextAttribute{}, 0, _renderTarget); // temporarily set size to 0 so it won't render. } catch (...) { return NTSTATUS_FROM_HRESULT(wil::ResultFromCaughtException()); } // Save cursor's relative height versus the viewport SHORT const sCursorHeightInViewportBefore = _textBuffer->GetCursor().GetPosition().Y - _viewport.Top(); // skip any drawing updates that might occur until we swap _textBuffer with the new buffer or we exit early. newTextBuffer->GetCursor().StartDeferDrawing(); _textBuffer->GetCursor().StartDeferDrawing(); // we're capturing _textBuffer by reference here because when we exit, we want to EndDefer on the current active buffer. auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); HRESULT hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get(), std::nullopt, std::nullopt); if (SUCCEEDED(hr)) { Cursor& newCursor = newTextBuffer->GetCursor(); // Adjust the viewport so the cursor doesn't wildly fly off up or down. SHORT const sCursorHeightInViewportAfter = newCursor.GetPosition().Y - _viewport.Top(); COORD coordCursorHeightDiff = { 0 }; coordCursorHeightDiff.Y = sCursorHeightInViewportAfter - sCursorHeightInViewportBefore; LOG_IF_FAILED(SetViewportOrigin(false, coordCursorHeightDiff, true)); _textBuffer->SetCurrentAttributes(oldPrimaryAttributes); _textBuffer.swap(newTextBuffer); } return NTSTATUS_FROM_HRESULT(hr); } // // Routine Description: // - This is the legacy screen resize with minimal changes // Arguments: // - coordNewScreenSize - new size of screen. // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeTraditional(const COORD coordNewScreenSize) { return NTSTATUS_FROM_HRESULT(_textBuffer->ResizeTraditional(coordNewScreenSize)); } // // Routine Description: // - This routine resizes the screen buffer. // Arguments: // - NewScreenSize - new size of screen in characters // - DoScrollBarUpdate - indicates whether to update scroll bars at the end // Return Value: // - Success if successful. Invalid parameter if screen buffer size is unexpected. No memory if allocation failed. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeScreenBuffer(const COORD coordNewScreenSize, const bool fDoScrollBarUpdate) { // If the size hasn't actually changed, do nothing. if (coordNewScreenSize == GetBufferSize().Dimensions()) { return STATUS_SUCCESS; } CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); NTSTATUS status = STATUS_SUCCESS; // If we're in conpty mode, suppress any immediate painting we might do // during the resize. if (gci.IsInVtIoMode()) { gci.GetVtIo()->BeginResize(); } auto endResize = wil::scope_exit([&] { if (gci.IsInVtIoMode()) { gci.GetVtIo()->EndResize(); } }); // cancel any active selection before resizing or it will not necessarily line up with the new buffer positions Selection::Instance().ClearSelection(); // cancel any popups before resizing or they will not necessarily line up with new buffer positions CommandLine::Instance().EndAllPopups(); const bool fWrapText = gci.GetWrapText(); if (fWrapText) { status = ResizeWithReflow(coordNewScreenSize); } else { status = NTSTATUS_FROM_HRESULT(ResizeTraditional(coordNewScreenSize)); } if (NT_SUCCESS(status)) { if (HasAccessibilityEventing()) { NotifyAccessibilityEventing(0, 0, (SHORT)(coordNewScreenSize.X - 1), (SHORT)(coordNewScreenSize.Y - 1)); } if ((!ConvScreenInfo)) { if (FAILED(ConsoleImeResizeCompStrScreenBuffer(coordNewScreenSize))) { // If something went wrong, just bail out. return STATUS_INVALID_HANDLE; } } // Fire off an event to let accessibility apps know the layout has changed. if (_pAccessibilityNotifier && IsActiveScreenBuffer()) { _pAccessibilityNotifier->NotifyConsoleLayoutEvent(); } if (fDoScrollBarUpdate) { UpdateScrollBars(); } ScreenBufferSizeChange(coordNewScreenSize); } return status; } // Routine Description: // - Given a rectangle containing screen buffer coordinates (character-level positioning, not pixel) // This method will trim the rectangle to ensure it is within the buffer. // For example, if the rectangle given has a right position of 85, but the current screen buffer // is only reaching from 0-79, then the right position will be set to 79. // Arguments: // - psrRect - Pointer to rectangle holding data to be trimmed // Return Value: // - void SCREEN_INFORMATION::ClipToScreenBuffer(_Inout_ SMALL_RECT* const psrClip) const { const auto bufferSize = GetBufferSize(); psrClip->Left = std::max(psrClip->Left, bufferSize.Left()); psrClip->Top = std::max(psrClip->Top, bufferSize.Top()); psrClip->Right = std::min(psrClip->Right, bufferSize.RightInclusive()); psrClip->Bottom = std::min(psrClip->Bottom, bufferSize.BottomInclusive()); } void SCREEN_INFORMATION::MakeCurrentCursorVisible() { MakeCursorVisible(_textBuffer->GetCursor().GetPosition()); } // Routine Description: // - This routine sets the cursor size and visibility both in the data // structures and on the screen. Also updates the cursor information of // this buffer's main buffer, if this buffer is an alt buffer. // Arguments: // - Size - cursor size // - Visible - cursor visibility // Return Value: // - None void SCREEN_INFORMATION::SetCursorInformation(const ULONG Size, const bool Visible) noexcept { Cursor& cursor = _textBuffer->GetCursor(); const auto originalSize = cursor.GetSize(); cursor.SetSize(Size); cursor.SetIsVisible(Visible); // If we are just trying to change the visibility, we don't want to reset // the cursor type. We only need to force it to the Legacy style if the // size is actually being changed. if (Size != originalSize) { cursor.SetType(CursorType::Legacy); } // If we're an alt buffer, also update our main buffer. // Users of the API expect both to be set - this can't be set by VT if (_psiMainBuffer) { _psiMainBuffer->SetCursorInformation(Size, Visible); } } // Routine Description: // - This routine sets the cursor shape both in the data // structures and on the screen. Also updates the cursor information of // this buffer's main buffer, if this buffer is an alt buffer. // Arguments: // - Type - The new shape to set the cursor to // - setMain - If true, propagate change to main buffer as well. // Return Value: // - None void SCREEN_INFORMATION::SetCursorType(const CursorType Type, const bool setMain) noexcept { Cursor& cursor = _textBuffer->GetCursor(); cursor.SetType(Type); // If we're an alt buffer, DON'T propagate this setting up to the main buffer. // We don't want to pollute that buffer with this state, // UNLESS we're getting called from the propsheet, then we DO want to update this. if (_psiMainBuffer && setMain) { _psiMainBuffer->SetCursorType(Type); } } // Routine Description: // - This routine sets a flag saying whether the cursor should be displayed // with its default size or it should be modified to indicate the // insert/overtype mode has changed. // Arguments: // - ScreenInfo - pointer to screen info structure. // - DoubleCursor - should we indicated non-normal mode // Return Value: // - None void SCREEN_INFORMATION::SetCursorDBMode(const bool DoubleCursor) { Cursor& cursor = _textBuffer->GetCursor(); if ((cursor.IsDouble() != DoubleCursor)) { cursor.SetIsDouble(DoubleCursor); } // If we're an alt buffer, also update our main buffer. if (_psiMainBuffer) { _psiMainBuffer->SetCursorDBMode(DoubleCursor); } } // Routine Description: // - This routine sets the cursor position in the data structures and on the screen. // Arguments: // - ScreenInfo - pointer to screen info structure. // - Position - new position of cursor // - TurnOn - true if cursor should be left on, false if should be left off // Return Value: // - Status [[nodiscard]] NTSTATUS SCREEN_INFORMATION::SetCursorPosition(const COORD Position, const bool TurnOn) { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Cursor& cursor = _textBuffer->GetCursor(); // // Ensure that the cursor position is within the constraints of the screen // buffer. // const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); if (Position.X >= coordScreenBufferSize.X || Position.Y >= coordScreenBufferSize.Y || Position.X < 0 || Position.Y < 0) { return STATUS_INVALID_PARAMETER; } // In GH#5291, we experimented with manually breaking the line on all cursor // movements here. As we print lines into the buffer, we mark lines as // wrapped when we print the last cell of the row, not the first cell of the // subsequent row (the row the first line wrapped onto). // // Logically, we thought that manually breaking lines when we move the // cursor was a good idea. We however, did not have the time to fully // validate that this was the correct answer, and a simpler solution for the // bug on hand was found. Furthermore, we thought it would be a more // comprehensive solution to only mark lines as wrapped when we print the // first cell of the second row, which would require some WriteCharsLegacy // work. cursor.SetPosition(Position); // If the cursor has moved below the virtual bottom, the bottom should be updated. if (Position.Y > _virtualBottom) { _virtualBottom = Position.Y; } // if we have the focus, adjust the cursor state if (gci.Flags & CONSOLE_HAS_FOCUS) { if (TurnOn) { cursor.SetDelay(false); cursor.SetIsOn(true); } else { cursor.SetDelay(true); } cursor.SetHasMoved(true); } return STATUS_SUCCESS; } void SCREEN_INFORMATION::MakeCursorVisible(const COORD CursorPosition, const bool updateBottom) { COORD WindowOrigin; if (CursorPosition.X > _viewport.RightInclusive()) { WindowOrigin.X = CursorPosition.X - _viewport.RightInclusive(); } else if (CursorPosition.X < _viewport.Left()) { WindowOrigin.X = CursorPosition.X - _viewport.Left(); } else { WindowOrigin.X = 0; } if (CursorPosition.Y > _viewport.BottomInclusive()) { WindowOrigin.Y = CursorPosition.Y - _viewport.BottomInclusive(); } else if (CursorPosition.Y < _viewport.Top()) { WindowOrigin.Y = CursorPosition.Y - _viewport.Top(); } else { WindowOrigin.Y = 0; } if (WindowOrigin.X != 0 || WindowOrigin.Y != 0) { LOG_IF_FAILED(SetViewportOrigin(false, WindowOrigin, updateBottom)); } } // Method Description: // - Sets the scroll margins for this buffer. // Arguments: // - margins: The new values of the scroll margins, *relative to the viewport* void SCREEN_INFORMATION::SetScrollMargins(const Viewport margins) { _scrollMargins = margins; } // Method Description: // - Returns the scrolling margins boundaries for this screen buffer, relative // to the origin of the text buffer. Most callers will want the absolute // positions of the margins, though they are set and stored relative to // origin of the viewport. // Arguments: // - Viewport SCREEN_INFORMATION::GetAbsoluteScrollMargins() const { return _viewport.ConvertFromOrigin(_scrollMargins); } // Method Description: // - Returns the scrolling margins boundaries for this screen buffer, relative // to the current viewport. // Arguments: // - Viewport SCREEN_INFORMATION::GetRelativeScrollMargins() const { return _scrollMargins; } // Routine Description: // - Retrieves the active buffer of this buffer. If this buffer has an // alternate buffer, this is the alternate buffer. Otherwise, it is this buffer. // Parameters: // - None // Return value: // - a reference to this buffer's active buffer. SCREEN_INFORMATION& SCREEN_INFORMATION::GetActiveBuffer() { return const_cast(static_cast(this)->GetActiveBuffer()); } const SCREEN_INFORMATION& SCREEN_INFORMATION::GetActiveBuffer() const { if (_psiAlternateBuffer != nullptr) { return *_psiAlternateBuffer; } return *this; } // Routine Description: // - Retrieves the main buffer of this buffer. If this buffer has an // alternate buffer, this is the main buffer. Otherwise, it is this buffer's main buffer. // The main buffer is not necessarily the active buffer. // Parameters: // - None // Return value: // - a reference to this buffer's main buffer. SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() { return const_cast(static_cast(this)->GetMainBuffer()); } const SCREEN_INFORMATION& SCREEN_INFORMATION::GetMainBuffer() const { if (_psiMainBuffer != nullptr) { return *_psiMainBuffer; } return *this; } // Routine Description: // - Instantiates a new buffer to be used as an alternate buffer. This buffer // does not have a driver handle associated with it and shares a state // machine with the main buffer it belongs to. // TODO: MSFT:19817348 Don't create alt screenbuffer's via an out SCREEN_INFORMATION** // Parameters: // - ppsiNewScreenBuffer - a pointer to receive the newly created buffer. // Return value: // - STATUS_SUCCESS if handled successfully. Otherwise, an appropriate status code indicating the error. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::_CreateAltBuffer(_Out_ SCREEN_INFORMATION** const ppsiNewScreenBuffer) { // Create new screen buffer. COORD WindowSize = _viewport.Dimensions(); const FontInfo& existingFont = GetCurrentFont(); // The buffer needs to be initialized with the standard erase attributes, // i.e. the current background color, but with no meta attributes set. auto initAttributes = GetAttributes(); initAttributes.SetStandardErase(); NTSTATUS Status = SCREEN_INFORMATION::CreateInstance(WindowSize, existingFont, WindowSize, initAttributes, GetPopupAttributes(), Cursor::CURSOR_SMALL_SIZE, ppsiNewScreenBuffer); if (NT_SUCCESS(Status)) { // Update the alt buffer's cursor style, visibility, and position to match our own. auto& myCursor = GetTextBuffer().GetCursor(); auto* const createdBuffer = *ppsiNewScreenBuffer; auto& altCursor = createdBuffer->GetTextBuffer().GetCursor(); altCursor.SetStyle(myCursor.GetSize(), myCursor.GetType()); altCursor.SetIsVisible(myCursor.IsVisible()); altCursor.SetBlinkingAllowed(myCursor.IsBlinkingAllowed()); // The new position should match the viewport-relative position of the main buffer. auto altCursorPos = myCursor.GetPosition(); altCursorPos.Y -= GetVirtualViewport().Top(); altCursor.SetPosition(altCursorPos); s_InsertScreenBuffer(createdBuffer); // delete the alt buffer's state machine. We don't want it. createdBuffer->_FreeOutputStateMachine(); // this has to be done before we give it a main buffer // we'll attach the GetSet, etc once we successfully make this buffer the active buffer. // Set up the new buffers references to our current state machine, dispatcher, getset, etc. createdBuffer->_stateMachine = _stateMachine; } return Status; } // Routine Description: // - Creates an "alternate" screen buffer for this buffer. In virtual terminals, there exists both a "main" // screen buffer and an alternate. ASBSET creates a new alternate, and switches to it. If there is an already // existing alternate, it is discarded. This allows applications to retain one HANDLE, and switch which buffer it points to seamlessly. // Parameters: // - None // Return value: // - STATUS_SUCCESS if handled successfully. Otherwise, an appropriate status code indicating the error. [[nodiscard]] NTSTATUS SCREEN_INFORMATION::UseAlternateScreenBuffer() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION& siMain = GetMainBuffer(); // If we're in an alt that resized, resize the main before making the new alt if (siMain._fAltWindowChanged) { siMain.ProcessResizeWindow(&(siMain._rcAltSavedClientNew), &(siMain._rcAltSavedClientOld)); siMain._fAltWindowChanged = false; } SCREEN_INFORMATION* psiNewAltBuffer; NTSTATUS Status = _CreateAltBuffer(&psiNewAltBuffer); if (NT_SUCCESS(Status)) { // if this is already an alternate buffer, we want to make the new // buffer the alt on our main buffer, not on ourself, because there // can only ever be one main and one alternate. SCREEN_INFORMATION* const psiOldAltBuffer = siMain._psiAlternateBuffer; psiNewAltBuffer->_psiMainBuffer = &siMain; siMain._psiAlternateBuffer = psiNewAltBuffer; if (psiOldAltBuffer != nullptr) { s_RemoveScreenBuffer(psiOldAltBuffer); // this will also delete the old alt buffer } ::SetActiveScreenBuffer(*psiNewAltBuffer); // Kind of a hack until we have proper signal channels: If the client app wants window size events, send one for // the new alt buffer's size (this is so WSL can update the TTY size when the MainSB.viewportWidth < // MainSB.bufferWidth (which can happen with wrap text disabled)) ScreenBufferSizeChange(psiNewAltBuffer->GetBufferSize().Dimensions()); // Tell the VT MouseInput handler that we're in the Alt buffer now gci.GetActiveInputBuffer()->GetTerminalInput().UseAlternateScreenBuffer(); } return Status; } // Routine Description: // - Restores the active buffer to be this buffer's main buffer. If this is the main buffer, then nothing happens. // Parameters: // - None // Return value: // - STATUS_SUCCESS if handled successfully. Otherwise, an appropriate status code indicating the error. void SCREEN_INFORMATION::UseMainScreenBuffer() { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); SCREEN_INFORMATION* psiMain = _psiMainBuffer; if (psiMain != nullptr) { if (psiMain->_fAltWindowChanged) { psiMain->ProcessResizeWindow(&(psiMain->_rcAltSavedClientNew), &(psiMain->_rcAltSavedClientOld)); psiMain->_fAltWindowChanged = false; } ::SetActiveScreenBuffer(*psiMain); psiMain->UpdateScrollBars(); // The alt had disabled scrollbars, re-enable them // send a _coordScreenBufferSizeChangeEvent for the new Sb viewport ScreenBufferSizeChange(psiMain->GetBufferSize().Dimensions()); SCREEN_INFORMATION* psiAlt = psiMain->_psiAlternateBuffer; psiMain->_psiAlternateBuffer = nullptr; // Copy the alt buffer's cursor style and visibility back to the main buffer. const auto& altCursor = psiAlt->GetTextBuffer().GetCursor(); auto& mainCursor = psiMain->GetTextBuffer().GetCursor(); mainCursor.SetStyle(altCursor.GetSize(), altCursor.GetType()); mainCursor.SetIsVisible(altCursor.IsVisible()); mainCursor.SetBlinkingAllowed(altCursor.IsBlinkingAllowed()); s_RemoveScreenBuffer(psiAlt); // this will also delete the alt buffer // deleting the alt buffer will give the GetSet back to its main // Tell the VT MouseInput handler that we're in the main buffer now gci.GetActiveInputBuffer()->GetTerminalInput().UseMainScreenBuffer(); } } // Routine Description: // - Helper indicating if the buffer has a main buffer, meaning that this is an alternate buffer. // Parameters: // - None // Return value: // - true iff this buffer has a main buffer. bool SCREEN_INFORMATION::_IsAltBuffer() const { return _psiMainBuffer != nullptr; } // Routine Description: // - Helper indicating if the buffer is acting as a pty - with the screenbuffer // clamped to the viewport size. This can be the case either when we're in // VT I/O mode, or when this buffer is an alt buffer. // Parameters: // - None // Return value: // - true iff this buffer has a main buffer. bool SCREEN_INFORMATION::_IsInPtyMode() const { const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); return _IsAltBuffer() || gci.IsInVtIoMode(); } // Routine Description: // - returns true if this buffer is in Virtual Terminal Output mode. // Parameters: // - None // Return Value: // - true iff this buffer is in Virtual Terminal Output mode. bool SCREEN_INFORMATION::_IsInVTMode() const { return WI_IsFlagSet(OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING); } // Routine Description: // - Returns the value of the attributes // Parameters: // // Return value: // - This screen buffer's attributes TextAttribute SCREEN_INFORMATION::GetAttributes() const { return _textBuffer->GetCurrentAttributes(); } // Routine Description: // - Returns the value of the popup attributes // Parameters: // // Return value: // - This screen buffer's popup attributes TextAttribute SCREEN_INFORMATION::GetPopupAttributes() const { return _PopupAttributes; } // Routine Description: // - Sets the value of the attributes on this screen buffer. Also propagates // the change down to the fill of the text buffer attached to this screen buffer. // Parameters: // - attributes - The new value of the attributes to use. // Return value: // void SCREEN_INFORMATION::SetAttributes(const TextAttribute& attributes) { if (_ignoreLegacyEquivalentVTAttributes) { // See the comment on StripErroneousVT16VersionsOfLegacyDefaults for more info. _textBuffer->SetCurrentAttributes(TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults(attributes)); return; } _textBuffer->SetCurrentAttributes(attributes); // If we're an alt buffer, DON'T propagate this setting up to the main buffer. // We don't want to pollute that buffer with this state. } // Method Description: // - Sets the value of the popup attributes on this screen buffer. // Parameters: // - popupAttributes - The new value of the popup attributes to use. // Return value: // void SCREEN_INFORMATION::SetPopupAttributes(const TextAttribute& popupAttributes) { _PopupAttributes = popupAttributes; // If we're an alt buffer, DON'T propagate this setting up to the main buffer. // We don't want to pollute that buffer with this state. } // Method Description: // - Sets the value of the attributes on this screen buffer. Also propagates // the change down to the fill of the attached text buffer. // - Additionally updates any popups to match the new color scheme. // - Also updates the defaults of the main buffer. This method is called by the // propsheet menu when you set the colors via the propsheet. In that // workflow, we want the main buffer's colors changed as well as our own. // Parameters: // - attributes - The new value of the attributes to use. // - popupAttributes - The new value of the popup attributes to use. // Return value: // // Notes: // This code is merged from the old global function SetScreenColors void SCREEN_INFORMATION::SetDefaultAttributes(const TextAttribute& attributes, const TextAttribute& popupAttributes) { CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const TextAttribute oldPrimaryAttributes = GetAttributes(); const TextAttribute oldPopupAttributes = GetPopupAttributes(); // Quick return if we don't need to do anything. if (oldPrimaryAttributes == attributes && oldPopupAttributes == popupAttributes) { return; } SetAttributes(attributes); SetPopupAttributes(popupAttributes); // Force repaint of entire viewport, unless we're in conpty mode. In that // case, we don't really need to force a redraw of the entire screen just // because the text attributes changed. if (!(gci.IsInVtIoMode())) { GetRenderTarget().TriggerRedrawAll(); } gci.ConsoleIme.RefreshAreaAttributes(); // If we're an alt buffer, also update our main buffer. if (_psiMainBuffer) { _psiMainBuffer->SetDefaultAttributes(attributes, popupAttributes); } } // Method Description: // - Returns an inclusive rectangle that describes the bounds of the buffer viewport. // Arguments: // - // Return Value: // - the viewport bounds as an inclusive rect. const Viewport& SCREEN_INFORMATION::GetViewport() const noexcept { return _viewport; } // Routine Description: // - This routine updates the size of the rectangle representing the viewport into the text buffer. // - It is specified in character count within the buffer. // - It will be corrected to not exceed the limits of the current screen buffer dimensions. // Arguments: // newViewport: The new viewport to use. If it's out of bounds in the negative // direction it will be shifted to positive coordinates. If it's bigger // that the screen buffer, it will be clamped to the size of the buffer. // updateBottom: if true, update our virtual bottom. This should be false when // called from UX interactions, such as scrolling with the mouse wheel, // and true when called from API endpoints, such as SetConsoleWindowInfo // Return Value: // - None void SCREEN_INFORMATION::SetViewport(const Viewport& newViewport, const bool updateBottom) { // make sure there's something to do if (newViewport == _viewport) { return; } // do adjustments on a copy that's easily manipulated. SMALL_RECT srCorrected = newViewport.ToExclusive(); if (srCorrected.Left < 0) { srCorrected.Right -= srCorrected.Left; srCorrected.Left = 0; } if (srCorrected.Top < 0) { srCorrected.Bottom -= srCorrected.Top; srCorrected.Top = 0; } const COORD coordScreenBufferSize = GetBufferSize().Dimensions(); if (srCorrected.Right > coordScreenBufferSize.X) { srCorrected.Right = coordScreenBufferSize.X; } if (srCorrected.Bottom > coordScreenBufferSize.Y) { srCorrected.Bottom = coordScreenBufferSize.Y; } _viewport = Viewport::FromExclusive(srCorrected); if (updateBottom) { UpdateBottom(); } Tracing::s_TraceWindowViewport(_viewport); } // Method Description: // - Performs a VT Erase All operation. In most terminals, this is done by // moving the viewport into the scrollback, clearing out the current screen. // For them, there can never be any characters beneath the viewport, as the // viewport is always at the bottom. So, we can accomplish the same behavior // by using the LastNonspaceCharacter as the "bottom", and placing the new // viewport underneath that character. // Parameters: // // Return value: // - S_OK if we succeeded, or another status if there was a failure. [[nodiscard]] HRESULT SCREEN_INFORMATION::VtEraseAll() { const COORD coordLastChar = _textBuffer->GetLastNonSpaceCharacter(); short sNewTop = coordLastChar.Y + 1; const Viewport oldViewport = _viewport; // Stash away the current position of the cursor within the viewport. // We'll need to restore the cursor to that same relative position, after // we move the viewport. const COORD oldCursorPos = _textBuffer->GetCursor().GetPosition(); COORD relativeCursor = oldCursorPos; oldViewport.ConvertToOrigin(&relativeCursor); short delta = (sNewTop + _viewport.Height()) - (GetBufferSize().Height()); for (auto i = 0; i < delta; i++) { _textBuffer->IncrementCircularBuffer(); sNewTop--; } const COORD coordNewOrigin = { 0, sNewTop }; RETURN_IF_FAILED(SetViewportOrigin(true, coordNewOrigin, true)); // Restore the relative cursor position _viewport.ConvertFromOrigin(&relativeCursor); RETURN_IF_FAILED(SetCursorPosition(relativeCursor, false)); // Update all the rows in the current viewport with the standard erase attributes, // i.e. the current background color, but with no meta attributes set. auto fillAttributes = GetAttributes(); fillAttributes.SetStandardErase(); auto fillPosition = COORD{ 0, _viewport.Top() }; auto fillLength = gsl::narrow_cast(_viewport.Height() * GetBufferSize().Width()); auto fillData = OutputCellIterator{ fillAttributes, fillLength }; Write(fillData, fillPosition, false); // Also reset the line rendition for the erased rows. _textBuffer->ResetLineRenditionRange(_viewport.Top(), _viewport.BottomExclusive()); return S_OK; } // Method Description: // - Clear the entire contents of the viewport, except for the cursor's row, // which is moved to the top line of the viewport. // - This is used exclusively by ConPTY to support GH#1193, GH#1882. This allows // a terminal to clear the contents of the ConPTY buffer, which is important // if the user would like to be able to clear the terminal-side buffer. // Arguments: // - // Return Value: // - S_OK [[nodiscard]] HRESULT SCREEN_INFORMATION::ClearBuffer() { const COORD oldCursorPos = _textBuffer->GetCursor().GetPosition(); short sNewTop = oldCursorPos.Y; const Viewport oldViewport = _viewport; short delta = (sNewTop + _viewport.Height()) - (GetBufferSize().Height()); for (auto i = 0; i < delta; i++) { _textBuffer->IncrementCircularBuffer(); sNewTop--; } const COORD coordNewOrigin = { 0, sNewTop }; RETURN_IF_FAILED(SetViewportOrigin(true, coordNewOrigin, true)); // Place the cursor at the same x coord, on the row that's now the top RETURN_IF_FAILED(SetCursorPosition(COORD{ oldCursorPos.X, sNewTop }, false)); // Update all the rows in the current viewport with the standard erase attributes, // i.e. the current background color, but with no meta attributes set. auto fillAttributes = GetAttributes(); fillAttributes.SetStandardErase(); // +1 on the y coord because we don't want to clear the attributes of the // cursor row, the one we saved. auto fillPosition = COORD{ 0, _viewport.Top() + 1 }; auto fillLength = gsl::narrow_cast(_viewport.Height() * GetBufferSize().Width()); auto fillData = OutputCellIterator{ fillAttributes, fillLength }; Write(fillData, fillPosition, false); _textBuffer->GetRenderTarget().TriggerRedrawAll(); // Also reset the line rendition for the erased rows. _textBuffer->ResetLineRenditionRange(_viewport.Top(), _viewport.BottomExclusive()); return S_OK; } // Method Description: // - Sets up the Output state machine to be in pty mode. Sequences it doesn't // understand will be written to the pTtyConnection passed in here. // Arguments: // - pTtyConnection: This is a TerminalOutputConnection that we can write the // sequence we didn't understand to. // Return Value: // - void SCREEN_INFORMATION::SetTerminalConnection(_In_ ITerminalOutputConnection* const pTtyConnection) { OutputStateMachineEngine& engine = reinterpret_cast(_stateMachine->Engine()); if (pTtyConnection) { engine.SetTerminalConnection(pTtyConnection, std::bind(&StateMachine::FlushToTerminal, _stateMachine.get())); } else { engine.SetTerminalConnection(nullptr, nullptr); } } // Routine Description: // - This routine copies a rectangular region from the screen buffer. no clipping is done. // Arguments: // - viewport - rectangle in source buffer to copy // Return Value: // - output cell rectangle copy of screen buffer data // Note: // - will throw exception on error. OutputCellRect SCREEN_INFORMATION::ReadRect(const Viewport viewport) const { // If the viewport given doesn't fit inside this screen, it's not a valid argument. THROW_HR_IF(E_INVALIDARG, !GetBufferSize().IsInBounds(viewport)); OutputCellRect result(viewport.Height(), viewport.Width()); const OutputCell paddingCell{ std::wstring_view{ &UNICODE_SPACE, 1 }, {}, GetAttributes() }; for (size_t rowIndex = 0; rowIndex < gsl::narrow(viewport.Height()); ++rowIndex) { COORD location = viewport.Origin(); location.Y += (SHORT)rowIndex; auto data = GetCellLineDataAt(location); const auto span = result.GetRow(rowIndex); auto it = span.begin(); // Copy row data while there still is data and we haven't run out of rect to store it into. while (data && it < span.end()) { *it++ = *data++; } // Pad out any remaining space. while (it < span.end()) { *it++ = paddingCell; } // if we're clipping a dbcs char then don't include it, add a space instead if (span.begin()->DbcsAttr().IsTrailing()) { *span.begin() = paddingCell; } if (span.rbegin()->DbcsAttr().IsLeading()) { *span.rbegin() = paddingCell; } } return result; } // Routine Description: // - Writes cells to the output buffer at the cursor position. // Arguments: // - it - Iterator representing output cell data to write. // Return Value: // - the iterator at its final position // Note: // - will throw exception on error. OutputCellIterator SCREEN_INFORMATION::Write(const OutputCellIterator it) { return _textBuffer->Write(it); } // Routine Description: // - Writes cells to the output buffer. // Arguments: // - it - Iterator representing output cell data to write. // - target - The position to start writing at // - wrap - change the wrap flag if we hit the end of the row while writing and there's still more data // Return Value: // - the iterator at its final position // Note: // - will throw exception on error. OutputCellIterator SCREEN_INFORMATION::Write(const OutputCellIterator it, const COORD target, const std::optional wrap) { // NOTE: if wrap = true/false, we want to set the line's wrap to true/false (respectively) if we reach the end of the line return _textBuffer->Write(it, target, wrap); } // Routine Description: // - This routine writes a rectangular region into the screen buffer. // Arguments: // - it - Iterator to the data to insert // - viewport - rectangular region for insertion // Return Value: // - the iterator at its final position // Note: // - will throw exception on error. OutputCellIterator SCREEN_INFORMATION::WriteRect(const OutputCellIterator it, const Viewport viewport) { THROW_HR_IF(E_INVALIDARG, viewport.Height() <= 0); THROW_HR_IF(E_INVALIDARG, viewport.Width() <= 0); OutputCellIterator iter = it; for (auto i = viewport.Top(); i < viewport.BottomExclusive(); i++) { iter = _textBuffer->WriteLine(iter, { viewport.Left(), i }, false, viewport.RightInclusive()); } return iter; } // Routine Description: // - This routine writes a rectangular region into the screen buffer. // Arguments: // - data - rectangular data in memory buffer // - location - origin point (top left corner) of where to write rectangular data // Return Value: // - // Note: // - will throw exception on error. void SCREEN_INFORMATION::WriteRect(const OutputCellRect& data, const COORD location) { for (size_t i = 0; i < data.Height(); i++) { const auto iter = data.GetRowIter(i); COORD point; point.X = location.X; point.Y = location.Y + static_cast(i); _textBuffer->WriteLine(iter, point); } } // Routine Description: // - Clears out the entire text buffer with the default character and // the current default attribute applied to this screen. void SCREEN_INFORMATION::ClearTextData() { // Clear the text buffer. _textBuffer->Reset(); } // Routine Description: // - finds the boundaries of the word at the given position on the screen // Arguments: // - position - location on the screen to get the word boundary for // Return Value: // - word boundary positions std::pair SCREEN_INFORMATION::GetWordBoundary(const COORD position) const { // The position argument is in screen coordinates, but we need the // equivalent buffer position, taking line rendition into account. COORD clampedPosition = _textBuffer->ScreenToBufferPosition(position); GetBufferSize().Clamp(clampedPosition); COORD start{ clampedPosition }; COORD end{ clampedPosition }; // find the start of the word auto startIt = GetTextLineDataAt(clampedPosition); while (startIt) { --startIt; if (!startIt || IsWordDelim(*startIt)) { break; } --start.X; } // find the end of the word auto endIt = GetTextLineDataAt(clampedPosition); while (endIt) { if (IsWordDelim(*endIt)) { break; } ++endIt; ++end.X; } // trim leading zeros if we need to const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); if (gci.GetTrimLeadingZeros()) { // Trim the leading zeros: 000fe12 -> fe12, except 0x and 0n. // Useful for debugging // Get iterator from the start of the selection auto trimIt = GetTextLineDataAt(start); // Advance to the second character to check if it's an x or n. trimIt++; // Only process if it's a single character. If it's a complicated run, then it's not an x or n. if (trimIt->size() == 1) { // Get the single character const auto wch = trimIt->front(); // If the string is long enough to have stuff after the 0x/0n and it doesn't have one... if (end.X > start.X + 2 && wch != L'x' && wch != L'X' && wch != L'n') { trimIt--; // Back up to the first character again // Now loop through and advance the selection forward each time // we find a single character '0' to Trim off the leading zeroes. while (trimIt->size() == 1 && trimIt->front() == L'0' && start.X < end.X - 1) { start.X++; trimIt++; } } } } // The calculated range is in buffer coordinates, but the caller is // expecting screen offsets, so we have to convert these back again. start = _textBuffer->BufferToScreenPosition(start); end = _textBuffer->BufferToScreenPosition(end); return { start, end }; } TextBuffer& SCREEN_INFORMATION::GetTextBuffer() noexcept { return *_textBuffer; } const TextBuffer& SCREEN_INFORMATION::GetTextBuffer() const noexcept { return *_textBuffer; } TextBufferTextIterator SCREEN_INFORMATION::GetTextDataAt(const COORD at) const { return _textBuffer->GetTextDataAt(at); } TextBufferCellIterator SCREEN_INFORMATION::GetCellDataAt(const COORD at) const { return _textBuffer->GetCellDataAt(at); } TextBufferTextIterator SCREEN_INFORMATION::GetTextLineDataAt(const COORD at) const { return _textBuffer->GetTextLineDataAt(at); } TextBufferCellIterator SCREEN_INFORMATION::GetCellLineDataAt(const COORD at) const { return _textBuffer->GetCellLineDataAt(at); } TextBufferTextIterator SCREEN_INFORMATION::GetTextDataAt(const COORD at, const Viewport limit) const { return _textBuffer->GetTextDataAt(at, limit); } TextBufferCellIterator SCREEN_INFORMATION::GetCellDataAt(const COORD at, const Viewport limit) const { return _textBuffer->GetCellDataAt(at, limit); } // Method Description: // - Updates our internal "virtual bottom" tracker with wherever the viewport // currently is. // - // Return Value: // - void SCREEN_INFORMATION::UpdateBottom() { _virtualBottom = _viewport.BottomInclusive(); } // Method Description: // - Initialize the row with the cursor on it to the standard erase attributes. // This is executed when we move the cursor below the current viewport in // VT mode. When that happens in a real terminal, the line is brand new, // so it gets initialized for the first time with the current attributes. // Our rows are usually pre-initialized, so re-initialize it here to // emulate that behavior. // See MSFT:17415310. // Arguments: // - // Return Value: // - void SCREEN_INFORMATION::InitializeCursorRowAttributes() { if (_textBuffer) { const auto& cursor = _textBuffer->GetCursor(); ROW& row = _textBuffer->GetRowByOffset(cursor.GetPosition().Y); // The VT standard requires that the new row is initialized with // the current background color, but with no meta attributes set. auto fillAttributes = GetAttributes(); fillAttributes.SetStandardErase(); row.GetAttrRow().SetAttrToEnd(0, fillAttributes); // The row should also be single width to start with. row.SetLineRendition(LineRendition::SingleWidth); } } // Method Description: // - Moves the viewport to where we last believed the "virtual bottom" was. This // emulates linux terminal behavior, where there's no buffer, only a // viewport. This is called by WriteChars, on output from an application in // VT mode, before the output is processed by the state machine. // This ensures that if a user scrolls around in the buffer, and a client // application uses VT to control the cursor/buffer, those commands are // still processed relative to the coordinates before the user scrolled the buffer. // Arguments: // - // Return Value: // - void SCREEN_INFORMATION::MoveToBottom() { const auto virtualView = GetVirtualViewport(); LOG_IF_NTSTATUS_FAILED(SetViewportOrigin(true, virtualView.Origin(), true)); } // Method Description: // - Returns the "virtual" Viewport - the viewport with its bottom at // `_virtualBottom`. For VT operations, this is essentially the mutable // section of the buffer. // Arguments: // - // Return Value: // - the virtual terminal viewport Viewport SCREEN_INFORMATION::GetVirtualViewport() const noexcept { const short newTop = _virtualBottom - _viewport.Height() + 1; return Viewport::FromDimensions({ _viewport.Left(), newTop }, _viewport.Dimensions()); } // Method Description: // - Returns true if the character at the cursor's current position is wide. // See IsGlyphFullWidth // Arguments: // - // Return Value: // - true if the character at the cursor's current position is wide bool SCREEN_INFORMATION::CursorIsDoubleWidth() const { const auto& buffer = GetTextBuffer(); const auto position = buffer.GetCursor().GetPosition(); TextBufferTextIterator it(TextBufferCellIterator(buffer, position)); return IsGlyphFullWidth(*it); } // Method Description: // - Retrieves this buffer's current render target. // Arguments: // - // Return Value: // - This buffer's current render target. IRenderTarget& SCREEN_INFORMATION::GetRenderTarget() noexcept { return _renderTarget; } // Method Description: // - Gets the current font of the screen buffer. // Arguments: // - // Return Value: // - A FontInfo describing our current font. FontInfo& SCREEN_INFORMATION::GetCurrentFont() noexcept { return _currentFont; } // Method Description: // See the non-const version of this function. const FontInfo& SCREEN_INFORMATION::GetCurrentFont() const noexcept { return _currentFont; } // Method Description: // - Gets the desired font of the screen buffer. If we try loading this font and // have to fallback to another, then GetCurrentFont()!=GetDesiredFont(). // We store this separately, so that if we need to reload the font, we can // try again with our prefered font info (in the desired font info) instead // of re-using the looked up value from before. // Arguments: // - // Return Value: // - A FontInfo describing our desired font. FontInfoDesired& SCREEN_INFORMATION::GetDesiredFont() noexcept { return _desiredFont; } // Method Description: // See the non-const version of this function. const FontInfoDesired& SCREEN_INFORMATION::GetDesiredFont() const noexcept { return _desiredFont; } // Method Description: // - Returns true iff the scroll margins have been set. // Arguments: // - // Return Value: // - true iff the scroll margins have been set. bool SCREEN_INFORMATION::AreMarginsSet() const noexcept { return _scrollMargins.BottomInclusive() > _scrollMargins.Top(); } // Routine Description: // - Determines whether a cursor position is within the vertical bounds of the // scroll margins, or the margins aren't set. // Parameters: // - cursorPosition - The cursor position to test // Return value: // - true iff the position is in bounds. bool SCREEN_INFORMATION::IsCursorInMargins(const COORD cursorPosition) const noexcept { // If the margins aren't set, then any position is considered in bounds. if (!AreMarginsSet()) { return true; } const auto margins = GetAbsoluteScrollMargins().ToInclusive(); return cursorPosition.Y <= margins.Bottom && cursorPosition.Y >= margins.Top; } // Routine Description: // - Engages the legacy VT handling quirk; see TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults void SCREEN_INFORMATION::SetIgnoreLegacyEquivalentVTAttributes() noexcept { _ignoreLegacyEquivalentVTAttributes = true; } // Routine Description: // - Disengages the legacy VT handling quirk; see TextAttribute::StripErroneousVT16VersionsOfLegacyDefaults void SCREEN_INFORMATION::ResetIgnoreLegacyEquivalentVTAttributes() noexcept { _ignoreLegacyEquivalentVTAttributes = false; }