terminal/src/renderer/vt/paint.cpp
James Holderness b604117421
Standardize the color table order (#11602)
## Summary of the Pull Request

In the original implementation, we used two different orderings for the color tables. The WT color table used ANSI order, while the conhost color table used a Windows-specific order. This PR standardizes on the ANSI color order everywhere, so the usage of indexed colors is consistent across both parts of the code base, which will hopefully allow more of the code to be shared one day.

## References

This is another small step towards de-duplicating `AdaptDispatch` and `TerminalDispatch` for issue #3849, and is essentially a followup to the SGR dispatch refactoring in PR #6728.

## PR Checklist
* [x] Closes #11461
* [x] CLA signed.
* [x] Tests added/passed
* [ ] Documentation updated.
* [ ] Schema updated.
* [x] I've discussed this with core contributors already. Issue number where discussion took place: #11461

## Detailed Description of the Pull Request / Additional comments

Conhost still needs to deal with legacy attributes using Windows color order, so those values now need to be transposed to ANSI colors order when creating a `TextAttribute` object. This is done with a simple mapping table, which also handles the translation of the default color entries, so it's actually slightly faster than the original code.

And when converting `TextAttribute` values back to legacy console attributes, we were already using a mapping table to handle the narrowing of 256-color values down to 16 colors, so we just needed to adjust that table to account for the translation from ANSI to Windows, and then could make use of the same table for both 256-color and 16-color values.

There are also a few places in conhost that read from or write to the color tables, and those now need to transpose the index values. I've addressed this by creating separate `SetLegacyColorTableEntry` and `GetLegacyColorTableEntry` methods in the `Settings` class which take care of the mapping, so it's now clearer in which cases the code is dealing with legacy values, and which are ANSI values.

These methods are used in the `SetConsoleScreenBufferInfoEx` and `GetConsoleScreenBufferInfoEx` APIs, as well as a few place where color preferences are handled (the registry, shortcut links, and the properties dialog), none of which are particularly sensitive to performance. However, we also use the legacy table when looking up the default colors for rendering (which happens a lot), so I've refactored that code so the default color calculations now only occur once per frame.

The plus side of all of this is that the VT code doesn't need to do the index translation anymore, so we can finally get rid of all the calls to `XTermToWindowsIndex`, and we no longer need a separate color table initialization method for conhost, so I was able to merge a number of color initialization methods into one. We also no longer need to translate from legacy values to ANSI when generating VT sequences for conpty.

The one exception to that is the 16-color VT renderer, which uses the `TextColor::GetLegacyIndex` method to approximate 16-color equivalents for RGB and 256-color values. Since that method returns a legacy index, it still needs to be translated to ANSI before it can be used in a VT sequence. But this should be no worse than it was before.

One more special case is conhost's secret _Color Selection_ feature. That uses `Ctrl`+Number and `Alt`+Number key sequences to highlight parts of the buffer, and the mapping from number to color is based on the Windows color order. So that mapping now needs to be transposed, but that's also not performance sensitive.

The only thing that I haven't bothered to update is the trace logging code in the `Telemetry` class, which logs the first 16 entries in the color table. Those entries are now going to be in a different order, but I didn't think that would be of great concern to anyone.

## Validation Steps Performed

A lot of unit tests needed to be updated to use ANSI color constants when setting indexed colors, where before they might have been expecting values in Windows order. But this replaced a wild mix of different constants, sometimes having to use bit shifting, as well as values mapped with `XTermToWindowsIndex`, so I think the tests are a whole lot clearer now. Only a few cases have been left with literal numbers where that seemed more appropriate.

In addition to getting the unit tests working, I've also manually tested the behaviour of all the console APIs which I thought could be affected by these changes, and confirmed that they produced the same results in the new code as they did in the original implementation.

This includes:
- `WriteConsoleOutput`
- `ReadConsoleOutput`
- `SetConsoleTextAttribute` with `WriteConsoleOutputCharacter`
- `FillConsoleOutputAttribute` and `FillConsoleOutputCharacter` 
- `ScrollConsoleScreenBuffer`
- `GetConsoleScreenBufferInfo`
- `GetConsoleScreenBufferInfoEx`
- `SetConsoleScreenBufferInfoEx`

I've also manually tested changing colors via the console properties menu, the registry, and shortcut links, including setting default colors and popup colors. And I've tested that the "Quirks Mode" is still working as expected in PowerShell.

In terms of performance, I wrote a little test app that filled a 80x9999 buffer with random color combinations using `WriteConsoleOutput`, which I figured was likely to be the most performance sensitive call, and I think it now actually performs slightly better than the original implementation.

I've also tested similar code - just filling the visible window - with SGR VT sequences of various types, and the performance seems about the same as it was before.
2021-11-04 22:13:22 +00:00

606 lines
23 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 = _invalidMap.any() ||
_scrollDelta != til::point{ 0, 0 } ||
_cursorMoved ||
_titleChanged;
_quickReturn = !somethingToDo;
_trace.TraceStartPaint(_quickReturn,
_invalidMap,
_lastViewport.ToInclusive(),
_scrollDelta,
_cursorMoved,
_wrappedRow);
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();
_invalidMap.reset_all();
_scrollDelta = { 0, 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.
// - lineWrapped: true if this run we're painting is the end of a line that
// wrapped. If we're not painting the last column of a wrapped line, then this
// will be false.
// Return Value:
// - S_OK or suitable HRESULT error from writing pipe.
[[nodiscard]] HRESULT VtEngine::PaintBufferLine(gsl::span<const Cluster> const clusters,
const COORD coord,
const bool /*trimLeft*/,
const bool /*lineWrapped*/) 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 GridLineSet /*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 CursorOptions& options) noexcept
{
_trace.TracePaintCursor(options.coordCursor);
// 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:
// - textAttributes: Text attributes to use for the colors.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_RgbUpdateDrawingBrushes(const TextAttribute& textAttributes) noexcept
{
const auto fg = textAttributes.GetForeground();
const auto bg = textAttributes.GetBackground();
auto lastFg = _lastTextAttributes.GetForeground();
auto lastBg = _lastTextAttributes.GetBackground();
// If both the FG and BG should be the defaults, emit a SGR reset.
if (fg.IsDefault() && bg.IsDefault() && !(lastFg.IsDefault() && lastBg.IsDefault()))
{
// SGR Reset will clear all attributes (except hyperlink ID) - which means
// we cannot reset _lastTextAttributes by simply doing
// _lastTextAttributes = {};
// because we want to retain the last hyperlink ID
RETURN_IF_FAILED(_SetGraphicsDefault());
_lastTextAttributes.SetDefaultBackground();
_lastTextAttributes.SetDefaultForeground();
_lastTextAttributes.SetDefaultMetaAttrs();
lastFg = {};
lastBg = {};
}
if (fg != lastFg)
{
if (fg.IsDefault())
{
RETURN_IF_FAILED(_SetGraphicsRenditionDefaultColor(true));
}
else if (fg.IsIndex16())
{
RETURN_IF_FAILED(_SetGraphicsRendition16Color(fg.GetIndex(), true));
}
else if (fg.IsIndex256())
{
RETURN_IF_FAILED(_SetGraphicsRendition256Color(fg.GetIndex(), true));
}
else if (fg.IsRgb())
{
RETURN_IF_FAILED(_SetGraphicsRenditionRGBColor(fg.GetRGB(), true));
}
_lastTextAttributes.SetForeground(fg);
}
if (bg != lastBg)
{
if (bg.IsDefault())
{
RETURN_IF_FAILED(_SetGraphicsRenditionDefaultColor(false));
}
else if (bg.IsIndex16())
{
RETURN_IF_FAILED(_SetGraphicsRendition16Color(bg.GetIndex(), false));
}
else if (bg.IsIndex256())
{
RETURN_IF_FAILED(_SetGraphicsRendition256Color(bg.GetIndex(), false));
}
else if (bg.IsRgb())
{
RETURN_IF_FAILED(_SetGraphicsRenditionRGBColor(bg.GetRGB(), false));
}
_lastTextAttributes.SetBackground(bg);
}
return S_OK;
}
// Routine Description:
// - Write a VT sequence to change the current colors of text. It will try to
// find ANSI colors that are nearest to the input colors, and write those
// indices to the pipe.
// Arguments:
// - textAttributes: Text attributes to use for the colors.
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT VtEngine::_16ColorUpdateDrawingBrushes(const TextAttribute& textAttributes) noexcept
{
const auto fg = textAttributes.GetForeground();
const auto bg = textAttributes.GetBackground();
auto lastFg = _lastTextAttributes.GetForeground();
auto lastBg = _lastTextAttributes.GetBackground();
// If either FG or BG have changed to default, emit a SGR reset.
// We can't reset FG and BG to default individually.
if ((fg.IsDefault() && !lastFg.IsDefault()) || (bg.IsDefault() && !lastBg.IsDefault()))
{
// SGR Reset will clear all attributes (except hyperlink ID) - which means
// we cannot reset _lastTextAttributes by simply doing
// _lastTextAttributes = {};
// because we want to retain the last hyperlink ID
RETURN_IF_FAILED(_SetGraphicsDefault());
_lastTextAttributes.SetDefaultBackground();
_lastTextAttributes.SetDefaultForeground();
_lastTextAttributes.SetDefaultMetaAttrs();
lastFg = {};
lastBg = {};
}
// We use the legacy color calculations to generate an approximation of the
// colors in the Windows 16-color table, but we need to transpose those
// values to obtain an index in an ANSI-compatible order.
auto fgIndex = TextColor::TransposeLegacyIndex(fg.GetLegacyIndex(0));
auto bgIndex = TextColor::TransposeLegacyIndex(bg.GetLegacyIndex(0));
// If the bold attribute is set, and the foreground can be brightened, then do so.
const bool brighten = textAttributes.IsBold() && fg.CanBeBrightened();
fgIndex |= (brighten ? FOREGROUND_INTENSITY : 0);
// To actually render bright colors, though, we need to use SGR bold.
const auto needBold = fgIndex > 7;
if (needBold != _lastTextAttributes.IsBold())
{
RETURN_IF_FAILED(_SetBold(needBold));
_lastTextAttributes.SetBold(needBold);
}
// After which we drop the high bits, since only colors 0 to 7 are supported.
fgIndex &= 7;
bgIndex &= 7;
if (!fg.IsDefault() && (lastFg.IsDefault() || fgIndex != lastFg.GetIndex()))
{
RETURN_IF_FAILED(_SetGraphicsRendition16Color(fgIndex, true));
_lastTextAttributes.SetIndexedForeground(fgIndex);
}
if (!bg.IsDefault() && (lastBg.IsDefault() || bgIndex != lastBg.GetIndex()))
{
RETURN_IF_FAILED(_SetGraphicsRendition16Color(bgIndex, false));
_lastTextAttributes.SetIndexedBackground(bgIndex);
}
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(gsl::span<const Cluster> const clusters,
const COORD coord) noexcept
{
try
{
RETURN_IF_FAILED(_MoveCursor(coord));
_bufferLine.clear();
_bufferLine.reserve(clusters.size());
short totalWidth = 0;
for (const auto& cluster : clusters)
{
_bufferLine.append(cluster.GetText());
RETURN_IF_FAILED(ShortAdd(totalWidth, gsl::narrow<short>(cluster.GetColumns()), &totalWidth));
}
RETURN_IF_FAILED(VtEngine::_WriteTerminalAscii(_bufferLine));
// 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(gsl::span<const Cluster> const clusters,
const COORD coord,
const bool lineWrapped) noexcept
{
if (coord.Y < _virtualTop)
{
return S_OK;
}
_bufferLine.clear();
_bufferLine.reserve(clusters.size());
short totalWidth = 0;
for (const auto& cluster : clusters)
{
_bufferLine.append(cluster.GetText());
RETURN_IF_FAILED(ShortAdd(totalWidth, static_cast<short>(cluster.GetColumns()), &totalWidth));
}
const size_t cchLine = _bufferLine.size();
bool foundNonspace = false;
size_t lastNonSpace = 0;
for (size_t i = 0; i < cchLine; i++)
{
if (_bufferLine.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);
const bool printingBottomLine = coord.Y == _lastViewport.BottomInclusive();
// GH#5502 - If the background color of the "new bottom line" is different
// than when we emitted the line, we can't optimize out the spaces from it.
// We'll still need to emit those spaces, so that the connected terminal
// will have the same background color on those blank cells.
const bool bgMatched = _newBottomLineBG.has_value() ? (_newBottomLineBG.value() == _lastTextAttributes.GetBackground()) : true;
// 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.
//
// GH#5161: Only removeSpaces when we're in the _newBottomLine state and the
// line we're trying to print right now _actually is the bottom line_
//
// GH#5291: DON'T remove spaces when the row wrapped. We might need those
// spaces to preserve the wrap state of this line, or the cursor position.
// For example, vim.exe uses "~ "... to clear the line, and then leaves
// the lines _wrapped_. It doesn't care to manually break the lines, but if
// we trimmed the spaces off here, we'd print all the "~"s one after another
// on the same line.
const bool removeSpaces = !lineWrapped && (useEraseChar ||
_clearedAllThisFrame ||
(_newBottomLine && printingBottomLine && bgMatched));
const size_t cchActual = removeSpaces ?
(cchLine - numSpaces) :
cchLine;
const size_t columnsActual = removeSpaces ?
(totalWidth - numSpaces) :
totalWidth;
if (cchActual == 0)
{
// If the previous row wrapped, but this line is empty, then we actually
// do want to move the cursor down. Otherwise, we'll possibly end up
// accidentally erasing the last character from the previous line, as
// the cursor is still waiting on that character for the next character
// to follow it.
//
// GH#5839 - If we've emitted a wrapped row, because the cursor is
// sitting just past the last cell of the previous row, if we execute a
// EraseCharacter or EraseLine here, then the row won't actually get
// cleared here. This logic is important to make sure that the cursor is
// in the right position before we do that.
_wrappedRow = std::nullopt;
_trace.TraceClearWrapped();
}
// Move the cursor to the start of this run.
RETURN_IF_FAILED(_MoveCursor(coord));
// Write the actual text string
RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8({ _bufferLine.data(), cchActual }));
// GH#4415, GH#5181
// If the renderer told us that this was a wrapped line, then mark
// that we've wrapped this line. The next time we attempt to move the
// cursor, if we're trying to move it to the start of the next line,
// we'll remember that this line was wrapped, and not manually break the
// line.
if (lineWrapped)
{
_wrappedRow = coord.Y;
_trace.TraceSetWrapped(coord.Y);
}
// Update our internal tracker of the cursor's position.
// See MSFT:20266233 (which is also GH#357)
// 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.
//
// GH#1245: This needs to be RightExclusive, _not_ inclusive. Otherwise, we
// won't update our internal cursor position tracker correctly at the last
// character of the row.
if (_lastText.X < _lastViewport.RightExclusive())
{
_lastText.X += static_cast<short>(columnsActual);
}
// GH#1245: If we wrote the exactly last char of the row, then we're in the
// "delayed EOL wrap" state. Different terminals (conhost, gnome-terminal,
// wt) all behave differently with how the cursor behaves at an end of line.
// Mark that we're in the delayed EOL wrap state - we don't want to be
// clever about how we move the cursor in this state, since different
// terminals will handle a backspace differently in this state.
if (_lastText.X >= _lastViewport.RightInclusive())
{
_delayedEolWrap = true;
}
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 && printingBottomLine)
{
// 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 if (numSpaces > 0 && removeSpaces) // if we deleted the spaces... re-add them
{
// TODO GH#5430 - Determine why and when we would do this.
std::wstring spaces = std::wstring(numSpaces, L' ');
RETURN_IF_FAILED(VtEngine::_WriteTerminalUtf8(spaces));
_lastText.X += static_cast<short>(numSpaces);
}
}
// If we printed to the bottom line, and we previously thought that this was
// a new bottom line, it certainly isn't new any longer.
if (printingBottomLine)
{
_newBottomLine = false;
_newBottomLineBG = std::nullopt;
}
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_view /*newTitle*/) noexcept
{
return S_OK;
}