terminal/src/host/screenInfo.cpp

2769 lines
102 KiB
C++

// 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 <math.h>
#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 },
_tabStops{},
_virtualBottom{ 0 },
_renderTarget{ *this },
_currentFont{ fontInfo },
_desiredFont{ fontInfo }
{
// 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();
THROW_HR_IF_NULL(E_FAIL, pNotifier);
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<TextBuffer>(coordScreenBufferSize,
defaultAttributes,
uiCursorSize,
pScreen->_renderTarget);
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
pScreen->_textBuffer->GetCursor().SetColor(gci.GetCursorColor());
pScreen->_textBuffer->GetCursor().SetType(gci.GetCursorType());
const NTSTATUS status = pScreen->_InitializeOutputStateMachine();
if (pScreen->_IsInVTMode())
{
// microsoft/terminal#411: If VT mode is enabled, lets construct the
// VT tab stops. Without this line, if a user has
// VirtualTerminalLevel set, then
// SetConsoleMode(ENABLE_VIRTUAL_TERMINAL_PROCESSING) won't set our
// tab stops, because we're never going from vt off -> on
pScreen->SetDefaultVtTabStops();
}
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:
// - <none>
// Return Value:
// - a viewport whos 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<ConhostInternalGetSet>(*this);
auto defaults = std::make_unique<WriteBuffer>(*this);
auto adapter = std::make_unique<AdaptDispatch>(std::move(getset), std::move(defaults));
auto engine = std::make_unique<OutputStateMachineEngine>(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<StateMachine>(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:
// - <none>
// 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:
// - <none>
// 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 = gci.GenerateLegacyAttributes(GetAttributes());
*pwPopupAttributes = gci.GenerateLegacyAttributes(_PopupAttributes);
// the copy length must be constant for now to keep OACR happy with buffer overruns.
memmove(lpColorTable, gci.GetColorTable(), COLOR_TABLE_SIZE * sizeof(COLORREF));
*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);
}
}
// 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)
{
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
// 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()),
gci.GenerateLegacyAttributes(cellData->TextAttr()));
_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.
_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:
// - <none>
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 rying 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:
// - <none>
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:
// - <none>
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:
// - <none>
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.
srNewViewport.Right = std::min(srNewViewport.Right, gsl::narrow<SHORT>(coordScreenBufferSize.X - 1));
srNewViewport.Bottom = std::min(srNewViewport.Bottom, gsl::narrow<SHORT>(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<short>(std::max(0, _virtualBottom - (newViewport.Height() - 1)));
newViewport = Viewport::FromDimensions(COORD({ newViewport.Left(), newTop }), newViewport.Dimensions());
}
_viewport = newViewport;
UpdateBottom();
Tracing::s_TraceWindowViewport(_viewport);
}
// 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:
// - <none>
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:
// - <none>
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:
// - <in> 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<TextBuffer> newTextBuffer;
try
{
newTextBuffer = std::make_unique<TextBuffer>(coordNewScreenSize,
GetAttributes(),
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();
HRESULT hr = TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get());
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.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))
{
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 (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:
// - <none>
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();
cursor.SetSize(Size);
cursor.SetIsVisible(Visible);
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 color. Also updates the cursor information of
// this buffer's main buffer, if this buffer is an alt buffer.
// Arguments:
// - Color - The new color to set the cursor to
// - setMain - If true, propagate change to main buffer as well.
// Return Value:
// - None
void SCREEN_INFORMATION::SetCursorColor(const unsigned int Color, const bool setMain) noexcept
{
Cursor& cursor = _textBuffer->GetCursor();
cursor.SetColor(Color);
// 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->SetCursorColor(Color);
}
}
// 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;
}
cursor.SetPosition(Position);
// 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:
// - <none>
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:
// - <none>
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<SCREEN_INFORMATION&>(static_cast<const SCREEN_INFORMATION* const>(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<SCREEN_INFORMATION&>(static_cast<const SCREEN_INFORMATION* const>(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 to match our own.
auto& myCursor = GetTextBuffer().GetCursor();
auto* const createdBuffer = *ppsiNewScreenBuffer;
createdBuffer->GetTextBuffer().GetCursor().SetStyle(myCursor.GetSize(), myCursor.GetColor(), myCursor.GetType());
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;
// Setup the alt buffer's tabs stops with the default tab stop settings
createdBuffer->SetDefaultVtTabStops();
}
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.terminalMouseInput.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;
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.terminalMouseInput.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:
// - Sets a VT tab stop in the column sColumn. If there is already a tab there, it does nothing.
// Parameters:
// - sColumn: the column to add a tab stop to.
// Return value:
// - none
// Note: may throw exception on allocation error
void SCREEN_INFORMATION::AddTabStop(const SHORT sColumn)
{
if (std::find(_tabStops.begin(), _tabStops.end(), sColumn) == _tabStops.end())
{
_tabStops.push_back(sColumn);
_tabStops.sort();
}
}
// Routine Description:
// - Clears all of the VT tabs that have been set. This also deletes them.
// Parameters:
// <none>
// Return value:
// <none>
void SCREEN_INFORMATION::ClearTabStops() noexcept
{
_tabStops.clear();
}
// Routine Description:
// - Clears the VT tab in the column sColumn (if one has been set). Also deletes it from the heap.
// Parameters:
// - sColumn - The column to clear the tab stop for.
// Return value:
// <none>
void SCREEN_INFORMATION::ClearTabStop(const SHORT sColumn) noexcept
{
_tabStops.remove(sColumn);
}
// Routine Description:
// - Places the location that a forwards tab would take cCurrCursorPos to into pcNewCursorPos
// Parameters:
// - cCurrCursorPos - The initial cursor location
// Return value:
// - <none>
COORD SCREEN_INFORMATION::GetForwardTab(const COORD cCurrCursorPos) const noexcept
{
COORD cNewCursorPos = cCurrCursorPos;
SHORT sWidth = GetBufferSize().RightInclusive();
if (_tabStops.empty() || cCurrCursorPos.X >= _tabStops.back())
{
cNewCursorPos.X = sWidth;
}
else
{
// search for next tab stop
for (auto it = _tabStops.cbegin(); it != _tabStops.cend(); ++it)
{
if (*it > cCurrCursorPos.X)
{
// make sure we don't exceed the width of the buffer
cNewCursorPos.X = std::min(*it, sWidth);
break;
}
}
}
return cNewCursorPos;
}
// Routine Description:
// - Places the location that a backwards tab would take cCurrCursorPos to into pcNewCursorPos
// Parameters:
// - cCurrCursorPos - The initial cursor location
// Return value:
// - <none>
COORD SCREEN_INFORMATION::GetReverseTab(const COORD cCurrCursorPos) const noexcept
{
COORD cNewCursorPos = cCurrCursorPos;
// if we're at 0, or there are NO tabs, or the first tab is farther right than where we are
if (cCurrCursorPos.X == 0 || _tabStops.empty() || _tabStops.front() >= cCurrCursorPos.X)
{
cNewCursorPos.X = 0;
}
else
{
for (auto it = _tabStops.crbegin(); it != _tabStops.crend(); ++it)
{
if (*it < cCurrCursorPos.X)
{
cNewCursorPos.X = *it;
break;
}
}
}
return cNewCursorPos;
}
// Routine Description:
// - Returns true if any VT-style tab stops have been set (with AddTabStop)
// Parameters:
// <none>
// Return value:
// - true if any VT-style tab stops have been set
bool SCREEN_INFORMATION::AreTabsSet() const noexcept
{
return !_tabStops.empty();
}
// Routine Description:
// - adds default tab stops for vt mode
void SCREEN_INFORMATION::SetDefaultVtTabStops()
{
_tabStops.clear();
const int width = GetBufferSize().RightInclusive();
FAIL_FAST_IF(width < 0);
for (int pos = 0; pos <= width; pos += TAB_SIZE)
{
_tabStops.push_back(gsl::narrow<short>(pos));
}
if (_tabStops.back() != width)
{
_tabStops.push_back(gsl::narrow<short>(width));
}
}
// Routine Description:
// - Returns the value of the attributes
// Parameters:
// <none>
// 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:
// <none>
// Return value:
// - This screen buffer's popup attributes
const TextAttribute* const 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:
// <none>
void SCREEN_INFORMATION::SetAttributes(const TextAttribute& attributes)
{
_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:
// <none>
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:
// <none>
// 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);
auto& commandLine = CommandLine::Instance();
if (commandLine.HasPopup())
{
commandLine.UpdatePopups(attributes, popupAttributes, oldPrimaryAttributes, oldPopupAttributes);
}
// force repaint of entire viewport
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:
// - <none>
// 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.ToInclusive();
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::FromInclusive(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:
// <none>
// 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<size_t>(_viewport.Height() * GetBufferSize().Width());
auto fillData = OutputCellIterator{ fillAttributes, fillLength };
Write(fillData, fillPosition, false);
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:
// - <none>
void SCREEN_INFORMATION::SetTerminalConnection(_In_ ITerminalOutputConnection* const pTtyConnection)
{
OutputStateMachineEngine& engine = reinterpret_cast<OutputStateMachineEngine&>(_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<size_t>(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<bool> 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:
// - <none>
// 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<short>(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<COORD, COORD> SCREEN_INFORMATION::GetWordBoundary(const COORD position) const
{
COORD clampedPosition = 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++;
}
}
}
}
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.
// - <none>
// Return Value:
// - <none>
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:
// - <none>
// Return Value:
// - <none>
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);
}
}
// 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:
// - <none>
// Return Value:
// - <none>
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:
// - <none>
// Return Value:
// - the virtual terminal viewport
Viewport SCREEN_INFORMATION::GetVirtualViewport() const noexcept
{
const short newTop = _virtualBottom - _viewport.Height() + 1;
return Viewport::FromDimensions({ 0, newTop }, _viewport.Dimensions());
}
// Method Description:
// - Returns true if the character at the cursor's current position is wide.
// See IsGlyphFullWidth
// Arguments:
// - <none>
// 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:
// - <none>
// 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:
// - <none>
// 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:
// - <none>
// 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:
// - <none>
// 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;
}
// Method Description:
// - Gets the region of the buffer that should be used for scrolling within the
// scroll margins. If the scroll margins aren't set, it returns the entire
// buffer size.
// Arguments:
// - <none>
// Return Value:
// - The area of the buffer within the scroll margins
Viewport SCREEN_INFORMATION::GetScrollingRegion() const noexcept
{
const auto buffer = GetBufferSize();
const bool marginsSet = AreMarginsSet();
const auto marginRect = GetAbsoluteScrollMargins().ToInclusive();
const auto margin = Viewport::FromInclusive({ buffer.Left(),
marginsSet ? marginRect.Top : buffer.Top(),
buffer.RightInclusive(),
marginsSet ? marginRect.Bottom : buffer.BottomInclusive() });
return margin;
}