4dd476ecbd
This reverts commit 56c35945b9
.
Also patches up some build fixes made after it and corrects a VtRendererTest that was dependent on the locks.
532 lines
20 KiB
C++
532 lines
20 KiB
C++
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT license.
|
|
|
|
#include "precomp.h"
|
|
|
|
#include "vtrenderer.hpp"
|
|
#include "../../inc/conattrs.hpp"
|
|
#include "../../types/inc/convert.hpp"
|
|
|
|
#pragma hdrstop
|
|
using namespace Microsoft::Console::Render;
|
|
using namespace Microsoft::Console::Types;
|
|
|
|
// Routine Description:
|
|
// - Prepares internal structures for a painting operation.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - S_OK if we started to paint. S_FALSE if we didn't need to paint.
|
|
// HRESULT error code if painting didn't start successfully.
|
|
[[nodiscard]] HRESULT VtEngine::StartPaint() noexcept
|
|
{
|
|
if (_pipeBroken)
|
|
{
|
|
return S_FALSE;
|
|
}
|
|
|
|
// If there's nothing to do, quick return
|
|
bool somethingToDo = _fInvalidRectUsed ||
|
|
(_scrollDelta.X != 0 || _scrollDelta.Y != 0) ||
|
|
_cursorMoved ||
|
|
_titleChanged;
|
|
|
|
_quickReturn = !somethingToDo;
|
|
_trace.TraceStartPaint(_quickReturn, _fInvalidRectUsed, _invalidRect, _lastViewport, _scrollDelta, _cursorMoved);
|
|
|
|
return _quickReturn ? S_FALSE : S_OK;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - EndPaint helper to perform the final cleanup after painting. If we
|
|
// returned S_FALSE from StartPaint, there's no guarantee this was called.
|
|
// That's okay however, EndPaint only zeros structs that would be zero if
|
|
// StartPaint returns S_FALSE.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - S_OK, else an appropriate HRESULT for failing to allocate or write.
|
|
[[nodiscard]] HRESULT VtEngine::EndPaint() noexcept
|
|
{
|
|
_trace.TraceEndPaint();
|
|
|
|
_invalidRect = Viewport::Empty();
|
|
_fInvalidRectUsed = false;
|
|
_scrollDelta = { 0 };
|
|
_clearedAllThisFrame = false;
|
|
_cursorMoved = false;
|
|
_firstPaint = false;
|
|
_skipCursor = false;
|
|
_resized = false;
|
|
// If we've circled the buffer this frame, move our virtual top upwards.
|
|
// We do this at the END of the frame, so that during the paint, we still
|
|
// use the original virtual top.
|
|
if (_circled)
|
|
{
|
|
if (_virtualTop > 0)
|
|
{
|
|
_virtualTop--;
|
|
}
|
|
}
|
|
_circled = false;
|
|
|
|
// If we deferred a cursor movement during the frame, make sure we put the
|
|
// cursor in the right place before we end the frame.
|
|
if (_deferredCursorPos != INVALID_COORDS)
|
|
{
|
|
RETURN_IF_FAILED(_MoveCursor(_deferredCursorPos));
|
|
}
|
|
|
|
RETURN_IF_FAILED(_Flush());
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Used to perform longer running presentation steps outside the lock so the
|
|
// other threads can continue.
|
|
// - Not currently used by VtEngine.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - S_FALSE since we do nothing.
|
|
[[nodiscard]] HRESULT VtEngine::Present() noexcept
|
|
{
|
|
return S_FALSE;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Paints the background of the invalid area of the frame.
|
|
// Arguments:
|
|
// - <none>
|
|
// Return Value:
|
|
// - S_OK
|
|
[[nodiscard]] HRESULT VtEngine::PaintBackground() noexcept
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Draws one line of the buffer to the screen. Writes the characters to the
|
|
// pipe. If the characters are outside the ASCII range (0-0x7f), then
|
|
// instead writes a '?'
|
|
// Arguments:
|
|
// - clusters - text and column count data to be written
|
|
// - trimLeft - This specifies whether to trim one character width off the left
|
|
// side of the output. Used for drawing the right-half only of a
|
|
// double-wide character.
|
|
// Return Value:
|
|
// - S_OK or suitable HRESULT error from writing pipe.
|
|
[[nodiscard]] HRESULT VtEngine::PaintBufferLine(std::basic_string_view<Cluster> const clusters,
|
|
const COORD coord,
|
|
const bool /*trimLeft*/) noexcept
|
|
{
|
|
return VtEngine::_PaintAsciiBufferLine(clusters, coord);
|
|
}
|
|
|
|
// Method Description:
|
|
// - Draws up to one line worth of grid lines on top of characters.
|
|
// Arguments:
|
|
// - lines - Enum defining which edges of the rectangle to draw
|
|
// - color - The color to use for drawing the edges.
|
|
// - cchLine - How many characters we should draw the grid lines along (left to right in a row)
|
|
// - coordTarget - The starting X/Y position of the first character to draw on.
|
|
// Return Value:
|
|
// - S_OK
|
|
[[nodiscard]] HRESULT VtEngine::PaintBufferGridLines(const GridLines /*lines*/,
|
|
const COLORREF /*color*/,
|
|
const size_t /*cchLine*/,
|
|
const COORD /*coordTarget*/) noexcept
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Draws the cursor on the screen
|
|
// Arguments:
|
|
// - options - Options that affect the presentation of the cursor
|
|
// Return Value:
|
|
// - S_OK or suitable HRESULT error from writing pipe.
|
|
[[nodiscard]] HRESULT VtEngine::PaintCursor(const IRenderEngine::CursorOptions& options) noexcept
|
|
{
|
|
// MSFT:15933349 - Send the terminal the updated cursor information, if it's changed.
|
|
LOG_IF_FAILED(_MoveCursor(options.coordCursor));
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Inverts the selected region on the current screen buffer.
|
|
// - Reads the selected area, selection mode, and active screen buffer
|
|
// from the global properties and dispatches a GDI invert on the selected text area.
|
|
// Because the selection is the responsibility of the terminal, and not the
|
|
// host, render nothing.
|
|
// Arguments:
|
|
// - rect - Rectangle to invert or highlight to make the selection area
|
|
// Return Value:
|
|
// - S_OK
|
|
[[nodiscard]] HRESULT VtEngine::PaintSelection(const SMALL_RECT /*rect*/) noexcept
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Write a VT sequence to change the current colors of text. Writes true RGB
|
|
// color sequences.
|
|
// Arguments:
|
|
// - colorForeground: The RGB Color to use to paint the foreground text.
|
|
// - colorBackground: The RGB Color to use to paint the background of the text.
|
|
// Return Value:
|
|
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
|
|
[[nodiscard]] HRESULT VtEngine::_RgbUpdateDrawingBrushes(const COLORREF colorForeground,
|
|
const COLORREF colorBackground,
|
|
const bool isBold,
|
|
_In_reads_(cColorTable) const COLORREF* const ColorTable,
|
|
const WORD cColorTable) noexcept
|
|
{
|
|
const bool fgChanged = colorForeground != _LastFG;
|
|
const bool bgChanged = colorBackground != _LastBG;
|
|
const bool fgIsDefault = colorForeground == _colorProvider.GetDefaultForeground();
|
|
const bool bgIsDefault = colorBackground == _colorProvider.GetDefaultBackground();
|
|
|
|
// If both the FG and BG should be the defaults, emit a SGR reset.
|
|
if ((fgChanged || bgChanged) && fgIsDefault && bgIsDefault)
|
|
{
|
|
// SGR Reset will also clear out the boldness of the text.
|
|
RETURN_IF_FAILED(_SetGraphicsDefault());
|
|
_LastFG = colorForeground;
|
|
_LastBG = colorBackground;
|
|
_lastWasBold = false;
|
|
|
|
// I'm not sure this is possible currently, but if the text is bold, but
|
|
// default colors, make sure we bold it.
|
|
if (isBold)
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsBoldness(isBold));
|
|
_lastWasBold = isBold;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_lastWasBold != isBold)
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsBoldness(isBold));
|
|
_lastWasBold = isBold;
|
|
}
|
|
|
|
WORD wFoundColor = 0;
|
|
if (fgChanged)
|
|
{
|
|
if (fgIsDefault)
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsRenditionDefaultColor(true));
|
|
}
|
|
else if (::FindTableIndex(colorForeground, ColorTable, cColorTable, &wFoundColor))
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsRendition16Color(wFoundColor, true));
|
|
}
|
|
else
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsRenditionRGBColor(colorForeground, true));
|
|
}
|
|
_LastFG = colorForeground;
|
|
}
|
|
|
|
if (bgChanged)
|
|
{
|
|
if (bgIsDefault)
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsRenditionDefaultColor(false));
|
|
}
|
|
else if (::FindTableIndex(colorBackground, ColorTable, cColorTable, &wFoundColor))
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsRendition16Color(wFoundColor, false));
|
|
}
|
|
else
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsRenditionRGBColor(colorBackground, false));
|
|
}
|
|
_LastBG = colorBackground;
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Write a VT sequence to change the current colors of text. It will try to
|
|
// find the colors in the color table that are nearest to the input colors,
|
|
// and write those indicies to the pipe.
|
|
// Arguments:
|
|
// - colorForeground: The RGB Color to use to paint the foreground text.
|
|
// - colorBackground: The RGB Color to use to paint the background of the text.
|
|
// - ColorTable: An array of colors to find the closest match to.
|
|
// - cColorTable: size of the color table.
|
|
// Return Value:
|
|
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
|
|
[[nodiscard]] HRESULT VtEngine::_16ColorUpdateDrawingBrushes(const COLORREF colorForeground,
|
|
const COLORREF colorBackground,
|
|
const bool isBold,
|
|
_In_reads_(cColorTable) const COLORREF* const ColorTable,
|
|
const WORD cColorTable) noexcept
|
|
{
|
|
const bool fgChanged = colorForeground != _LastFG;
|
|
const bool bgChanged = colorBackground != _LastBG;
|
|
const bool fgIsDefault = colorForeground == _colorProvider.GetDefaultForeground();
|
|
const bool bgIsDefault = colorBackground == _colorProvider.GetDefaultBackground();
|
|
|
|
// If both the FG and BG should be the defaults, emit a SGR reset.
|
|
if ((fgChanged || bgChanged) && fgIsDefault && bgIsDefault)
|
|
{
|
|
// SGR Reset will also clear out the boldness of the text.
|
|
RETURN_IF_FAILED(_SetGraphicsDefault());
|
|
_LastFG = colorForeground;
|
|
_LastBG = colorBackground;
|
|
_lastWasBold = false;
|
|
// I'm not sure this is possible currently, but if the text is bold, but
|
|
// default colors, make sure we bold it.
|
|
if (isBold)
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsBoldness(isBold));
|
|
_lastWasBold = isBold;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_lastWasBold != isBold)
|
|
{
|
|
RETURN_IF_FAILED(_SetGraphicsBoldness(isBold));
|
|
_lastWasBold = isBold;
|
|
}
|
|
|
|
if (fgChanged)
|
|
{
|
|
const WORD wNearestFg = ::FindNearestTableIndex(colorForeground, ColorTable, cColorTable);
|
|
RETURN_IF_FAILED(_SetGraphicsRendition16Color(wNearestFg, true));
|
|
|
|
_LastFG = colorForeground;
|
|
}
|
|
|
|
if (bgChanged)
|
|
{
|
|
const WORD wNearestBg = ::FindNearestTableIndex(colorBackground, ColorTable, cColorTable);
|
|
RETURN_IF_FAILED(_SetGraphicsRendition16Color(wNearestBg, false));
|
|
|
|
_LastBG = colorBackground;
|
|
}
|
|
}
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Draws one line of the buffer to the screen. Writes the characters to the
|
|
// pipe. If the characters are outside the ASCII range (0-0x7f), then
|
|
// instead writes a '?'.
|
|
// This is needed because the Windows internal telnet client implementation
|
|
// doesn't know how to handle >ASCII characters. The old telnetd would
|
|
// just replace them with '?' characters. If we render the >ASCII
|
|
// characters to telnet, it will likely end up drawing them wrong, which
|
|
// will make the client appear buggy and broken.
|
|
// Arguments:
|
|
// - clusters - text and column width data to be written
|
|
// - coord - character coordinate target to render within viewport
|
|
// Return Value:
|
|
// - S_OK or suitable HRESULT error from writing pipe.
|
|
[[nodiscard]] HRESULT VtEngine::_PaintAsciiBufferLine(std::basic_string_view<Cluster> const clusters,
|
|
const COORD coord) noexcept
|
|
{
|
|
try
|
|
{
|
|
RETURN_IF_FAILED(_MoveCursor(coord));
|
|
|
|
std::wstring wstr;
|
|
wstr.reserve(clusters.size());
|
|
|
|
short totalWidth = 0;
|
|
for (const auto& cluster : clusters)
|
|
{
|
|
wstr.append(cluster.GetText());
|
|
RETURN_IF_FAILED(ShortAdd(totalWidth, gsl::narrow<short>(cluster.GetColumns()), &totalWidth));
|
|
}
|
|
|
|
RETURN_IF_FAILED(VtEngine::_WriteTerminalAscii(wstr));
|
|
|
|
// Update our internal tracker of the cursor's position
|
|
_lastText.X += totalWidth;
|
|
|
|
return S_OK;
|
|
}
|
|
CATCH_RETURN();
|
|
}
|
|
|
|
// Routine Description:
|
|
// - Draws one line of the buffer to the screen. Writes the characters to the
|
|
// pipe, encoded in UTF-8.
|
|
// Arguments:
|
|
// - clusters - text and column widths to be written
|
|
// - coord - character coordinate target to render within viewport
|
|
// Return Value:
|
|
// - S_OK or suitable HRESULT error from writing pipe.
|
|
[[nodiscard]] HRESULT VtEngine::_PaintUtf8BufferLine(std::basic_string_view<Cluster> const clusters,
|
|
const COORD coord) noexcept
|
|
{
|
|
if (coord.Y < _virtualTop)
|
|
{
|
|
return S_OK;
|
|
}
|
|
|
|
RETURN_IF_FAILED(_MoveCursor(coord));
|
|
|
|
std::wstring unclusteredString;
|
|
unclusteredString.reserve(clusters.size());
|
|
short totalWidth = 0;
|
|
for (const auto& cluster : clusters)
|
|
{
|
|
unclusteredString.append(cluster.GetText());
|
|
RETURN_IF_FAILED(ShortAdd(totalWidth, static_cast<short>(cluster.GetColumns()), &totalWidth));
|
|
}
|
|
const size_t cchLine = unclusteredString.size();
|
|
|
|
bool foundNonspace = false;
|
|
size_t lastNonSpace = 0;
|
|
for (size_t i = 0; i < cchLine; i++)
|
|
{
|
|
if (unclusteredString.at(i) != L'\x20')
|
|
{
|
|
lastNonSpace = i;
|
|
foundNonspace = true;
|
|
}
|
|
}
|
|
// Examples:
|
|
// - " ":
|
|
// cch = 2, lastNonSpace = 0, foundNonSpace = false
|
|
// cch-lastNonSpace = 2 -> good
|
|
// cch-lastNonSpace-(0) = 2 -> good
|
|
// - "A "
|
|
// cch = 2, lastNonSpace = 0, foundNonSpace = true
|
|
// cch-lastNonSpace = 2 -> bad
|
|
// cch-lastNonSpace-(1) = 1 -> good
|
|
// - "AA"
|
|
// cch = 2, lastNonSpace = 1, foundNonSpace = true
|
|
// cch-lastNonSpace = 1 -> bad
|
|
// cch-lastNonSpace-(1) = 0 -> good
|
|
const size_t numSpaces = cchLine - lastNonSpace - (foundNonspace ? 1 : 0);
|
|
|
|
// Optimizations:
|
|
// If there are lots of spaces at the end of the line, we can try to Erase
|
|
// Character that number of spaces, then move the cursor forward (to
|
|
// where it would be if we had written the spaces)
|
|
// An erase character and move right sequence is 8 chars, and possibly 10
|
|
// (if there are at least 10 spaces, 2 digits to print)
|
|
// ESC [ %d X ESC [ %d C
|
|
// ESC [ %d %d X ESC [ %d %d C
|
|
// So we need at least 9 spaces for the optimized sequence to make sense.
|
|
// Also, if we already erased the entire display this frame, then
|
|
// don't do ANYTHING with erasing at all.
|
|
|
|
// Note: We're only doing these optimizations along the UTF-8 path, because
|
|
// the inbox telnet client doesn't understand the Erase Character sequence,
|
|
// and it uses xterm-ascii. This ensures that xterm and -256color consumers
|
|
// get the enhancements, and telnet isn't broken.
|
|
const bool optimalToUseECH = numSpaces > ERASE_CHARACTER_STRING_LENGTH;
|
|
const bool useEraseChar = (optimalToUseECH) &&
|
|
(!_newBottomLine) &&
|
|
(!_clearedAllThisFrame);
|
|
|
|
// If we're not using erase char, but we did erase all at the start of the
|
|
// frame, don't add spaces at the end.
|
|
const bool removeSpaces = (useEraseChar || (_clearedAllThisFrame) || (_newBottomLine));
|
|
const size_t cchActual = removeSpaces ?
|
|
(cchLine - numSpaces) :
|
|
cchLine;
|
|
|
|
const size_t columnsActual = removeSpaces ?
|
|
(totalWidth - numSpaces) :
|
|
totalWidth;
|
|
|
|
// Write the actual text string
|
|
std::wstring wstr = std::wstring(unclusteredString.data(), cchActual);
|
|
RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8(wstr));
|
|
|
|
// Update our internal tracker of the cursor's position.
|
|
// See MSFT:20266233
|
|
// If the cursor is at the rightmost column of the terminal, and we write a
|
|
// space, the cursor won't actually move to the next cell (which would
|
|
// be {0, _lastText.Y++}). The cursor will stay visibly in that last
|
|
// cell until then next character is output.
|
|
// If in that case, we increment the cursor position here (such that the X
|
|
// position would be one past the right of the terminal), when we come
|
|
// back through to MoveCursor in the last PaintCursor of the frame,
|
|
// we'll determine that we need to emit a \b to put the cursor in the
|
|
// right position. This is wrong, and will cause us to move the cursor
|
|
// back one character more than we wanted.
|
|
if (_lastText.X < _lastViewport.RightInclusive())
|
|
{
|
|
_lastText.X += static_cast<short>(columnsActual);
|
|
}
|
|
|
|
short sNumSpaces;
|
|
try
|
|
{
|
|
sNumSpaces = gsl::narrow<short>(numSpaces);
|
|
}
|
|
CATCH_RETURN();
|
|
|
|
if (useEraseChar)
|
|
{
|
|
// ECH doesn't actually move the cursor itself. However, we think that
|
|
// the cursor *should* be at the end of the area we just erased. Stash
|
|
// that position as our new deferred position. If we don't move the
|
|
// cursor somewhere else before the end of the frame, we'll move the
|
|
// cursor to the deferred position at the end of the frame, or right
|
|
// before we need to print new text.
|
|
_deferredCursorPos = { _lastText.X + sNumSpaces, _lastText.Y };
|
|
|
|
if (_deferredCursorPos.X < _lastViewport.RightInclusive())
|
|
{
|
|
RETURN_IF_FAILED(_EraseCharacter(sNumSpaces));
|
|
}
|
|
else
|
|
{
|
|
RETURN_IF_FAILED(_EraseLine());
|
|
}
|
|
}
|
|
else if (_newBottomLine)
|
|
{
|
|
// If we're on a new line, then we don't need to erase the line. The
|
|
// line is already empty.
|
|
if (optimalToUseECH)
|
|
{
|
|
_deferredCursorPos = { _lastText.X + sNumSpaces, _lastText.Y };
|
|
}
|
|
else
|
|
{
|
|
std::wstring spaces = std::wstring(numSpaces, L' ');
|
|
RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8(spaces));
|
|
|
|
_lastText.X += static_cast<short>(numSpaces);
|
|
}
|
|
}
|
|
|
|
// If we previously though that this was a new bottom line, it certainly
|
|
// isn't new any longer.
|
|
_newBottomLine = false;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// Method Description:
|
|
// - Updates the window's title string. Emits the VT sequence to SetWindowTitle.
|
|
// Because wintelnet does not understand these sequences by default, we
|
|
// don't do anything by default. Other modes can implement if they support
|
|
// the sequence.
|
|
// Arguments:
|
|
// - newTitle: the new string to use for the title of the window
|
|
// Return Value:
|
|
// - S_OK
|
|
[[nodiscard]] HRESULT VtEngine::_DoUpdateTitle(const std::wstring& /*newTitle*/) noexcept
|
|
{
|
|
return S_OK;
|
|
}
|