Skip to content

Commit

Permalink
Add support for the rectangular area operations (#14285)
Browse files Browse the repository at this point in the history
## Summary of the Pull Request

This PR adds support for the rectangular area escape sequences:
`DECCRA`, `DECFRA`, `DECERA`, `DECSERA`, `DECCARA`, `DECRARA`, and
`DECSACE`. They provide VT applications with an efficient way to copy,
fill, erase, or change the attributes in a rectangular area of the
screen.

## PR Checklist
* [x] Closes #14112
* [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: #14112

## Detailed Description of the Pull Request / Additional comments

All of these operations take a rectangle, defined by four coordinates.
These need to have defaults applied, potentially need to be clipped
and/or clamped within the active margins, and finally converted to
absolute buffer coordinates. To avoid having to repeat that boilerplate
code everywhere, I've pulled that functionality out into a shared method
which they all use.

With that out of the way, operations like `DECFRA` (fill), `DECERA`
(erase), and `DECSERA` (selective erase) are fairly simple. They're just
filling the given rectangle using the existing methods `_FillRect` and
`_SelectiveEraseRect`. `DECCRA` (copy) is a little more work, because we
didn't have existing code for that in `AdaptDispatch`, but it's mostly
just cloned from the conhost `_CopyRectangle` function.

The `DECCARA` (change attributes) and `DECRARA` (reverse attributes)
operations are different though. Their coordinates can be interpreted as
either a rectangle, or a stream of character positions (determined by
the `DECSACE` escape sequence), and they both deal with attribute
manipulation of the target area. So again I've pulled out that common
functionality into some shared methods.

They both also take a list of `SGR` options which define the attribute
changes that they need to apply to the target area. To parse that data,
I've had to refactor the `SGR` decoder from the `SetGraphicsRendition`
method so it could be used with a given `TextAttribute` instance instead
of just modifying the active attributes.

The way that works in `DECCARA`, we apply the `SGR` options to two
`TextAttribute` instances - one with all rendition bits on, and one with
all off - producing a pair of bit masks. Then by `AND`ing the target
attributes with the first bit mask, and `OR`ing them with the second, we
can efficiently achieve the same effect as if we'd applied each `SGR`
option to our target cells one by one.

In the case of `DECRARA`, we only need to create a single bit mask to
achieve the "reverse attribute" effect. That bit mask is applied to the
target cells with an `XOR` operation.

## Validation Steps Performed

Thanks to @KalleOlaviNiemitalo, we've been able to run a series of tests
on a real VT420, so we have a good idea of how these ops are intended to
work. Our implementation does a reasonably good job of matching that
behavior, but we don't yet support paging, so we don't have the `DECCRA`
ability to copy between pages, and we also don't have the concept of
"unoccupied" cells, so we can't support that aspect of the streaming
operations.

It's also worth mentioning that the VT420 doesn't have colors, so we
can't be sure exactly how they are meant to interpreted. However, based
on the way the other attribute are handled, and what we know from the
DEC STD 070 documentation, I think it's fair to assume that our handling
of colors is also reasonable.
  • Loading branch information
j4james committed Nov 10, 2022
1 parent 1b09ae3 commit 88c3ef6
Show file tree
Hide file tree
Showing 14 changed files with 833 additions and 183 deletions.
2 changes: 1 addition & 1 deletion src/buffer/out/TextAttribute.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ void TextAttribute::SetDefaultBackground() noexcept
// except the Protected attribute.
void TextAttribute::SetDefaultRenditionAttributes() noexcept
{
_attrs &= CharacterAttributes::Protected;
_attrs &= ~CharacterAttributes::Rendition;
}

// Method Description:
Expand Down
4 changes: 4 additions & 0 deletions src/buffer/out/TextAttribute.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ class TextAttribute final
void SetReverseVideo(bool isReversed) noexcept;
void SetProtected(bool isProtected) noexcept;

constexpr void SetCharacterAttributes(const CharacterAttributes attrs) noexcept
{
_attrs = attrs;
}
constexpr CharacterAttributes GetCharacterAttributes() const noexcept
{
return _attrs;
Expand Down
126 changes: 126 additions & 0 deletions src/host/ut_host/ScreenBufferTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,8 @@ class ScreenBufferTests
TEST_METHOD(TestReflowBiggerLongLineWithColor);

TEST_METHOD(TestDeferredMainBufferResize);

TEST_METHOD(RectangularAreaOperations);
};

void ScreenBufferTests::SingleAlternateBufferCreationTest()
Expand Down Expand Up @@ -7333,3 +7335,127 @@ void ScreenBufferTests::TestDeferredMainBufferResize()
VERIFY_ARE_EQUAL(altPostResizeView, mainPostRestoreView);
VERIFY_ARE_EQUAL(expectedSize, mainPostRestoreView);
}

void ScreenBufferTests::RectangularAreaOperations()
{
BEGIN_TEST_METHOD_PROPERTIES()
TEST_METHOD_PROPERTY(L"Data:rectOp", L"{0, 1, 2, 3, 4, 5}")
END_TEST_METHOD_PROPERTIES();

enum RectOp : int
{
DECFRA,
DECERA,
DECSERA,
DECCARA,
DECRARA,
DECCRA
};

RectOp rectOp;
VERIFY_SUCCEEDED(TestData::TryGetValue(L"rectOp", (int&)rectOp));

auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer().GetActiveBuffer();
auto& stateMachine = si.GetStateMachine();
WI_SetFlag(si.OutputMode, ENABLE_VIRTUAL_TERMINAL_PROCESSING);

const auto bufferWidth = si.GetBufferSize().Width();
const auto bufferHeight = si.GetBufferSize().Height();

// Move the viewport down a few lines, and only cover part of the buffer width.
si.SetViewport(Viewport::FromDimensions({ 5, 10 }, { bufferWidth - 10, 20 }), true);
const auto viewport = si.GetViewport();

// Fill the entire buffer with Zs. Blue on Green and Underlined.
const auto bufferChar = L'Z';
auto bufferAttr = TextAttribute{ FOREGROUND_BLUE | BACKGROUND_GREEN };
bufferAttr.SetUnderlined(true);
_FillLines(0, bufferHeight, bufferChar, bufferAttr);

// Set the active attributes to Red on Blue and Intense;
auto activeAttr = TextAttribute{ FOREGROUND_RED | BACKGROUND_BLUE };
activeAttr.SetIntense(true);
si.SetAttributes(activeAttr);

// The area we're targetting in all the operations below is 27;3 to 54;6.
// But VT coordinates use origin 1;1 so we need to subtract 1, and til::rect
// expects exclusive coordinates, so the bottom/right also need to add 1.
const auto targetArea = til::rect{ 27 - 1, viewport.Top() + 3 - 1, 54, viewport.Top() + 6 };

wchar_t expectedChar{};
TextAttribute expectedAttr;

// DECCARA and DECRARA can apply to both a stream of character positions or
// a rectangular area, but these tests only cover the rectangular option.
if (rectOp == DECCARA || rectOp == DECRARA)
{
Log::Comment(L"Request a rectangular change extent with DECSACE");
stateMachine.ProcessString(L"\033[2*x");
}

switch (rectOp)
{
case DECFRA:
Log::Comment(L"DECFRA: fill a rectangle with the active attributes and a given character value");
expectedAttr = activeAttr;
expectedChar = wchar_t{ 42 };
// The first parameter specifies the fill character.
stateMachine.ProcessString(L"\033[42;3;27;6;54$x");
break;
case DECERA:
Log::Comment(L"DECERA: erase a rectangle using the active colors but no rendition attributes");
expectedAttr = activeAttr;
expectedAttr.SetStandardErase();
expectedChar = L' ';
stateMachine.ProcessString(L"\033[3;27;6;54$z");
break;
case DECSERA:
Log::Comment(L"DECSERA: erase the text in a rectangle but leave the attributes unchanged");
expectedAttr = bufferAttr;
expectedChar = L' ';
stateMachine.ProcessString(L"\033[3;27;6;54${");
break;
case DECCARA:
Log::Comment(L"DECCARA: update the attributes in a rectangle but leave the text unchanged");
expectedAttr = bufferAttr;
expectedAttr.SetReverseVideo(true);
expectedChar = bufferChar;
// The final parameter specifies the reverse video attribute that will be set.
stateMachine.ProcessString(L"\033[3;27;6;54;7$r");
break;
case DECRARA:
Log::Comment(L"DECRARA: reverse the attributes in a rectangle but leave the text unchanged");
expectedAttr = bufferAttr;
expectedAttr.SetUnderlined(false);
expectedChar = bufferChar;
// The final parameter specifies the underline attribute that will be reversed.
stateMachine.ProcessString(L"\033[3;27;6;54;4$t");
break;
case DECCRA:
Log::Comment(L"DECCRA: copy a rectangle from the lower part of the viewport to the top");
expectedAttr = TextAttribute{ FOREGROUND_GREEN | BACKGROUND_RED };
expectedChar = L'*';
// Fill the lower part of the viewport with some different content.
_FillLines(viewport.Top() + 10, viewport.BottomExclusive(), expectedChar, expectedAttr);
// Copy a rectangle from that lower part up to the top with DECCRA.
stateMachine.ProcessString(L"\033[11;27;14;54;1;3;27;1;4$v");
// Reset the lower part back to it's original content.
_FillLines(viewport.Top() + 10, viewport.BottomExclusive(), bufferChar, bufferAttr);
break;
default:
VERIFY_FAIL(L"Unsupported operation");
}

const auto expectedChars = std::wstring(targetArea.width(), expectedChar);
VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.left, targetArea.top, targetArea.bottom, expectedChars, expectedAttr));

Log::Comment(L"Everything above and below the target area should remain unchanged");
VERIFY_IS_TRUE(_ValidateLinesContain(0, targetArea.top, bufferChar, bufferAttr));
VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.bottom, bufferHeight, bufferChar, bufferAttr));

Log::Comment(L"Everything to the left and right of the target area should remain unchanged");
const auto bufferChars = std::wstring(targetArea.left, bufferChar);
VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.top, targetArea.bottom, bufferChars, bufferAttr));
VERIFY_IS_TRUE(_ValidateLinesContain(targetArea.right, targetArea.top, targetArea.bottom, bufferChar, bufferAttr));
}
5 changes: 4 additions & 1 deletion src/inc/conattrs.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ enum class CharacterAttributes : uint16_t
RightGridline = COMMON_LVB_GRID_RVERTICAL, // 0x1000
Protected = 0x2000,
ReverseVideo = COMMON_LVB_REVERSE_VIDEO, // 0x4000
BottomGridline = COMMON_LVB_UNDERSCORE // 0x8000
BottomGridline = COMMON_LVB_UNDERSCORE, // 0x8000

All = 0xFFFF, // All character attributes
Rendition = All & ~Protected // Only rendition attributes (everything except Protected)
};
DEFINE_ENUM_FLAG_OPERATORS(CharacterAttributes);

Expand Down
7 changes: 7 additions & 0 deletions src/terminal/adapter/DispatchTypes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,13 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes
Scrollback = 3
};

enum class ChangeExtent : VTInt
{
Default = 0,
Stream = 1,
Rectangle = 2
};

enum class TaskbarState : VTInt
{
Clear = 0,
Expand Down
8 changes: 8 additions & 0 deletions src/terminal/adapter/ITermDispatch.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch
virtual bool SelectiveEraseInDisplay(const DispatchTypes::EraseType eraseType) = 0; // DECSED
virtual bool SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) = 0; // DECSEL

virtual bool ChangeAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs) = 0; // DECCARA
virtual bool ReverseAttributesRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTParameters attrs) = 0; // DECRARA
virtual bool CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt page, const VTInt dstTop, const VTInt dstLeft, const VTInt dstPage) = 0; // DECCRA
virtual bool FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) = 0; // DECFRA
virtual bool EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) = 0; // DECERA
virtual bool SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) = 0; // DECSERA
virtual bool SelectAttributeChangeExtent(const DispatchTypes::ChangeExtent changeExtent) = 0; // DECSACE

virtual bool SetGraphicsRendition(const VTParameters options) = 0; // SGR
virtual bool SetLineRendition(const LineRendition rendition) = 0; // DECSWL, DECDWL, DECDHL
virtual bool SetCharacterProtectionAttribute(const VTParameters options) = 0; // DECSCA
Expand Down

0 comments on commit 88c3ef6

Please sign in to comment.