terminal/src/types/viewport.cpp

1035 lines
42 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "inc/Viewport.hpp"
using namespace Microsoft::Console::Types;
Viewport::Viewport(const SMALL_RECT sr) noexcept :
_sr(sr)
{
}
Viewport::Viewport(const Viewport& other) noexcept :
_sr(other._sr)
{
}
Viewport Viewport::Empty() noexcept
{
return Viewport({ 0, 0, -1, -1 });
}
Viewport Viewport::FromInclusive(const SMALL_RECT sr) noexcept
{
return Viewport(sr);
}
Viewport Viewport::FromExclusive(const SMALL_RECT sr) noexcept
{
SMALL_RECT _sr = sr;
_sr.Bottom -= 1;
_sr.Right -= 1;
return Viewport::FromInclusive(_sr);
}
// Function Description:
// - Creates a new Viewport at the given origin, with the given dimensions.
// Arguments:
// - origin: The origin of the new Viewport. Becomes the Viewport's Left, Top
// - width: The width of the new viewport
// - height: The height of the new viewport
// Return Value:
// - a new Viewport at the given origin, with the given dimensions.
Viewport Viewport::FromDimensions(const COORD origin,
const short width,
const short height) noexcept
{
return Viewport::FromExclusive({ origin.X, origin.Y, origin.X + width, origin.Y + height });
}
// Function Description:
// - Creates a new Viewport at the given origin, with the given dimensions.
// Arguments:
// - origin: The origin of the new Viewport. Becomes the Viewport's Left, Top
// - dimensions: A coordinate containing the width and height of the new viewport
// in the x and y coordinates respectively.
// Return Value:
// - a new Viewport at the given origin, with the given dimensions.
Viewport Viewport::FromDimensions(const COORD origin,
const COORD dimensions) noexcept
{
return Viewport::FromExclusive({ origin.X, origin.Y, origin.X + dimensions.X, origin.Y + dimensions.Y });
}
// Function Description:
// - Creates a new Viewport at the origin, with the given dimensions.
// Arguments:
// - dimensions: A coordinate containing the width and height of the new viewport
// in the x and y coordinates respectively.
// Return Value:
// - a new Viewport at the origin, with the given dimensions.
Viewport Viewport::FromDimensions(const COORD dimensions) noexcept
{
return Viewport::FromDimensions({ 0 }, dimensions);
}
// Method Description:
// - Creates a Viewport equivalent to a 1x1 rectangle at the given coordinate.
// Arguments:
// - origin: origin of the rectangle to create.
// Return Value:
// - a 1x1 Viewport at the given coordinate
Viewport Viewport::FromCoord(const COORD origin) noexcept
{
return Viewport::FromInclusive({ origin.X, origin.Y, origin.X, origin.Y });
}
SHORT Viewport::Left() const noexcept
{
return _sr.Left;
}
SHORT Viewport::RightInclusive() const noexcept
{
return _sr.Right;
}
SHORT Viewport::RightExclusive() const noexcept
{
return _sr.Right + 1;
}
SHORT Viewport::Top() const noexcept
{
return _sr.Top;
}
SHORT Viewport::BottomInclusive() const noexcept
{
return _sr.Bottom;
}
SHORT Viewport::BottomExclusive() const noexcept
{
return _sr.Bottom + 1;
}
SHORT Viewport::Height() const noexcept
{
return BottomExclusive() - Top();
}
SHORT Viewport::Width() const noexcept
{
return RightExclusive() - Left();
}
// Method Description:
// - Get a coord representing the origin of this viewport.
// Arguments:
// - <none>
// Return Value:
// - the coordinates of this viewport's origin.
COORD Viewport::Origin() const noexcept
{
return { Left(), Top() };
}
// Method Description:
// - Get a coord representing the dimensions of this viewport.
// Arguments:
// - <none>
// Return Value:
// - the dimensions of this viewport.
COORD Viewport::Dimensions() const noexcept
{
return { Width(), Height() };
}
// Method Description:
// - Determines if the given viewport fits within this viewport.
// Arguments:
// - other - The viewport to fit inside this one
// Return Value:
// - True if it fits. False otherwise.
bool Viewport::IsInBounds(const Viewport& other) const noexcept
{
return other.Left() >= Left() && other.Left() <= RightInclusive() &&
other.RightInclusive() >= Left() && other.RightInclusive() <= RightInclusive() &&
other.Top() >= Top() && other.Top() <= other.BottomInclusive() &&
other.BottomInclusive() >= Top() && other.BottomInclusive() <= BottomInclusive();
}
// Method Description:
// - Determines if the given coordinate position lies within this viewport.
// Arguments:
// - pos - Coordinate position
// Return Value:
// - True if it lies inside the viewport. False otherwise.
bool Viewport::IsInBounds(const COORD& pos) const noexcept
{
return pos.X >= Left() && pos.X < RightExclusive() &&
pos.Y >= Top() && pos.Y < BottomExclusive();
}
// Method Description:
// - Clamps a coordinate position into the inside of this viewport.
// Arguments:
// - pos - coordinate to update/clamp
// Return Value:
// - <none>
void Viewport::Clamp(COORD& pos) const
{
THROW_HR_IF(E_NOT_VALID_STATE, !IsValid()); // we can't clamp to an invalid viewport.
pos.X = std::clamp(pos.X, Left(), RightInclusive());
pos.Y = std::clamp(pos.Y, Top(), BottomInclusive());
}
// Method Description:
// - Clamps a viewport into the inside of this viewport.
// Arguments:
// - other - Viewport to clamp to the inside of this viewport
// Return Value:
// - Clamped viewport
Viewport Viewport::Clamp(const Viewport& other) const noexcept
{
auto clampMe = other.ToInclusive();
clampMe.Left = std::clamp(clampMe.Left, Left(), RightInclusive());
clampMe.Right = std::clamp(clampMe.Right, Left(), RightInclusive());
clampMe.Top = std::clamp(clampMe.Top, Top(), BottomInclusive());
clampMe.Bottom = std::clamp(clampMe.Bottom, Top(), BottomInclusive());
return Viewport::FromInclusive(clampMe);
}
// Method Description:
// - Moves the coordinate given by the number of positions and
// in the direction given (repeated increment or decrement)
// Arguments:
// - move - Magnitude and direction of the move
// - pos - The coordinate position to adjust
// Return Value:
// - True if we successfully moved the requested distance. False if we had to stop early.
// - If False, we will restore the original position to the given coordinate.
bool Viewport::MoveInBounds(const ptrdiff_t move, COORD& pos) const noexcept
{
const auto backup = pos;
bool success = true; // If nothing happens, we're still successful (e.g. add = 0)
for (int i = 0; i < move; i++)
{
success = IncrementInBounds(pos);
// If an operation fails, break.
if (!success)
{
break;
}
}
for (int i = 0; i > move; i--)
{
success = DecrementInBounds(pos);
// If an operation fails, break.
if (!success)
{
break;
}
}
// If any operation failed, revert to backed up state.
if (!success)
{
pos = backup;
}
return success;
}
// Method Description:
// - Increments the given coordinate within the bounds of this viewport.
// Arguments:
// - pos - Coordinate position that will be incremented, if it can be.
// Return Value:
// - True if it could be incremented. False if it would move outside.
bool Viewport::IncrementInBounds(COORD& pos) const noexcept
{
return WalkInBounds(pos, { XWalk::LeftToRight, YWalk::TopToBottom });
}
// Method Description:
// - Increments the given coordinate within the bounds of this viewport
// rotating around to the top when reaching the bottom right corner.
// Arguments:
// - pos - Coordinate position that will be incremented.
// Return Value:
// - True if it could be incremented inside the viewport.
// - False if it rolled over from the bottom right corner back to the top.
bool Viewport::IncrementInBoundsCircular(COORD& pos) const noexcept
{
return WalkInBoundsCircular(pos, { XWalk::LeftToRight, YWalk::TopToBottom });
}
// Method Description:
// - Decrements the given coordinate within the bounds of this viewport.
// Arguments:
// - pos - Coordinate position that will be incremented, if it can be.
// Return Value:
// - True if it could be incremented. False if it would move outside.
bool Viewport::DecrementInBounds(COORD& pos) const noexcept
{
return WalkInBounds(pos, { XWalk::RightToLeft, YWalk::BottomToTop });
}
// Method Description:
// - Decrements the given coordinate within the bounds of this viewport
// rotating around to the bottom right when reaching the top left corner.
// Arguments:
// - pos - Coordinate position that will be decremented.
// Return Value:
// - True if it could be decremented inside the viewport.
// - False if it rolled over from the top left corner back to the bottom right.
bool Viewport::DecrementInBoundsCircular(COORD& pos) const noexcept
{
return WalkInBoundsCircular(pos, { XWalk::RightToLeft, YWalk::BottomToTop });
}
// Routine Description:
// - Compares two coordinate positions to determine whether they're the same, left, or right within the given buffer size
// Arguments:
// - first- The first coordinate position
// - second - The second coordinate position
// Return Value:
// - Negative if First is to the left of the Second.
// - 0 if First and Second are the same coordinate.
// - Positive if First is to the right of the Second.
// - This is so you can do s_CompareCoords(first, second) <= 0 for "first is left or the same as second".
// (the < looks like a left arrow :D)
// - The magnitude of the result is the distance between the two coordinates when typing characters into the buffer (left to right, top to bottom)
int Viewport::CompareInBounds(const COORD& first, const COORD& second) const noexcept
{
// Assert that our coordinates are within the expected boundaries
FAIL_FAST_IF(!IsInBounds(first));
FAIL_FAST_IF(!IsInBounds(second));
// First set the distance vertically
// If first is on row 4 and second is on row 6, first will be -2 rows behind second * an 80 character row would be -160.
// For the same row, it'll be 0 rows * 80 character width = 0 difference.
int retVal = (first.Y - second.Y) * Width();
// Now adjust for horizontal differences
// If first is in position 15 and second is in position 30, first is -15 left in relation to 30.
retVal += (first.X - second.X);
// Further notes:
// If we already moved behind one row, this will help correct for when first is right of second.
// For example, with row 4, col 79 and row 5, col 0 as first and second respectively, the distance is -1.
// Assume the row width is 80.
// Step one will set the retVal as -80 as first is one row behind the second.
// Step two will then see that first is 79 - 0 = +79 right of second and add 79
// The total is -80 + 79 = -1.
return retVal;
}
// Method Description:
// - Walks the given coordinate within the bounds of this viewport in the specified
// X and Y directions.
// Arguments:
// - pos - Coordinate position that will be adjusted, if it can be.
// - dir - Walking direction specifying which direction to go when reaching the end of a row/column
// Return Value:
// - True if it could be adjusted as specified and remain in bounds. False if it would move outside.
bool Viewport::WalkInBounds(COORD& pos, const WalkDir dir) const noexcept
{
auto copy = pos;
if (WalkInBoundsCircular(copy, dir))
{
pos = copy;
return true;
}
else
{
return false;
}
}
// Method Description:
// - Walks the given coordinate within the bounds of this viewport
// rotating around to the opposite corner when reaching the final corner
// in the specified direction.
// Arguments:
// - pos - Coordinate position that will be adjusted.
// - dir - Walking direction specifying which direction to go when reaching the end of a row/column
// Return Value:
// - True if it could be adjusted inside the viewport.
// - False if it rolled over from the final corner back to the initial corner
// for the specified walk direction.
bool Viewport::WalkInBoundsCircular(COORD& pos, const WalkDir dir) const noexcept
{
// Assert that the position given fits inside this viewport.
FAIL_FAST_IF(!IsInBounds(pos));
if (dir.x == XWalk::LeftToRight)
{
if (pos.X == RightInclusive())
{
pos.X = Left();
if (dir.y == YWalk::TopToBottom)
{
pos.Y++;
if (pos.Y > BottomInclusive())
{
pos.Y = Top();
return false;
}
}
else
{
pos.Y--;
if (pos.Y < Top())
{
pos.Y = BottomInclusive();
return false;
}
}
}
else
{
pos.X++;
}
}
else
{
if (pos.X == Left())
{
pos.X = RightInclusive();
if (dir.y == YWalk::TopToBottom)
{
pos.Y++;
if (pos.Y > BottomInclusive())
{
pos.Y = Top();
return false;
}
}
else
{
pos.Y--;
if (pos.Y < Top())
{
pos.Y = BottomInclusive();
return false;
}
}
}
else
{
pos.X--;
}
}
return true;
}
// Routine Description:
// - If walking through a viewport, one might want to know the origin
// for the direction walking.
// - For example, for walking up and to the left (bottom right corner
// to top left corner), the origin would start at the bottom right.
// Arguments:
// - dir - The direction one intends to walk through the viewport
// Return Value:
// - The origin for the walk to reach every position without circling
// if using this same viewport with the `WalkInBounds` methods.
COORD Viewport::GetWalkOrigin(const WalkDir dir) const noexcept
{
COORD origin;
origin.X = dir.x == XWalk::LeftToRight ? Left() : RightInclusive();
origin.Y = dir.y == YWalk::TopToBottom ? Top() : BottomInclusive();
return origin;
}
// Routine Description:
// - Given two viewports that will be used for copying data from one to the other (source, target),
// determine which direction you will have to walk through them to ensure that an overlapped copy
// won't erase data in the source that hasn't yet been read and copied into the target at the same
// coordinate offset position from their respective origins.
// - Note: See elaborate ASCII-art comment inside the body of this function for more details on how/why this works.
// Arguments:
// - source - The viewport representing the region that will be copied from
// - target - The viewport representing the region that will be copied to
// Return Value:
// - The direction to walk through both viewports from the walk origins to touch every cell and not
// accidentally overwrite something that hasn't been read yet. (use with GetWalkOrigin and WalkInBounds)
Viewport::WalkDir Viewport::DetermineWalkDirection(const Viewport& source, const Viewport& target) noexcept
{
// We can determine which direction we need to walk based on solely the origins of the two rectangles.
// I'll use a few examples to prove the situation.
//
// For the cardinal directions, let's start with this sample:
//
// source target
// origin 0,0 origin 4,0
// | |
// v V
// +--source-----+--target--------- +--source-----+--target---------
// | A B C D | E | 1 2 3 4 | becomes | A B C D | A | B C D E |
// | F G H I | J | 5 6 7 8 | =========> | F G H I | F | G H I J |
// | K L M N | O | 9 $ % @ | | K L M N | K | L M N O |
// -------------------------------- --------------------------------
//
// The source and target overlap in the 5th column (X=4).
// To ensure that we don't accidentally write over the source
// data before we copy it into the target, we want to start by
// reading that column (a.k.a. writing to the farthest away column
// of the target).
//
// This means we want to copy from right to left.
// Top to bottom and bottom to top don't really matter for this since it's
// a cardinal direction shift.
//
// If we do the right most column first as so...
//
// +--source-----+--target--------- +--source-----+--target---------
// | A B C D | E | 1 2 3 4 | step 1 | A B C D | E | 1 2 3 E |
// | F G H I | J | 5 6 7 8 | =========> | F G H I | J | 5 6 7 J |
// | K L M N | O | 9 $ % @ | | K L M N | O | 9 $ % O |
// -------------------------------- --------------------------------
//
// ... then we can see that the EJO column is safely copied first out of the way and
// can be overwritten on subsequent steps without losing anything.
// The rest of the columns aren't overlapping, so they'll be fine.
//
// But we extrapolate this logic to follow for rectangles that overlap more columns, up
// to and including only leaving one column not overlapped...
//
// source target
// origin origin
// 0,0 / 1,0
// | /
// v v
// +----+------target- +----+------target-
// | A | B C D | E | becomes | A | A B C | D |
// | F | G H I | J | =========> | F | F G H | I |
// | K | L M N | O | | K | K L M | N |
// ---source---------- ---source----------
//
// ... will still be OK following the same Right-To-Left rule as the first move.
//
// +----+------target- +----+------target-
// | A | B C D | E | step 1 | A | B C D | D |
// | F | G H I | J | =========> | F | G H I | I |
// | K | L M N | O | | K | L M N | N |
// ---source---------- ---source----------
//
// The DIN column from the source was moved to the target as the right most column
// of both rectangles. Now it is safe to iterate to the second column from the right
// and proceed with moving CHM on top of the source DIN as it was already moved.
//
// +----+------target- +----+------target-
// | A | B C D | E | step 2 | A | B C C | D |
// | F | G H I | J | =========> | F | G H H | I |
// | K | L M N | O | | K | L M M | N |
// ---source---------- ---source----------
//
// Continue walking right to left (an exercise left to the reader,) and we never lose
// any source data before it reaches the target with the Right To Left pattern.
//
// We notice that the target origin was Right of the source origin in this circumstance,
// (target origin X is > source origin X)
// so it is asserted that targets right of sources means that we should "walk" right to left.
//
// Reviewing the above, it doesn't appear to matter if we go Top to Bottom or Bottom to Top,
// so the conclusion is drawn that it doesn't matter as long as the source and target origin
// Y values are the same.
//
// Also, extrapolating this cardinal direction move to the other 3 cardinal directions,
// it should follow that they would follow the same rules.
// That is, a target left of a source, or a Westbound move, opposite of the above Eastbound move,
// should be "walked" left to right.
// (target origin X is < source origin X)
//
// We haven't given the sample yet that Northbound and Southbound moves are the same, but we
// could reason that the same logic applies and the conclusion would be a Northbound move
// would walk from the target toward the source again... a.k.a. Top to Bottom.
// (target origin Y is < source origin Y)
// Then the Southbound move would be the opposite, Bottom to Top.
// (target origin Y is > source origin Y)
//
// To confirm, let's try one more example but moving both at once in an ordinal direction Northeast.
//
// target
// origin 1, 0
// |
// v
// +----target-- +----target--
// source A | B C | A | D E |
// origin-->+------------ | becomes +------------ |
// 0, 1 | D | E | F | =========> | D | G | H |
// | ------------- | -------------
// | G H | I | G H | I
// --source----- --source-----
//
// Following our supposed rules from above, we have...
// Source Origin X = 0, Y = 1
// Target Origin X = 1, Y = 0
//
// Source Origin X < Target Origin X which means Right to Left
// Source Origin Y > Target Origin Y which means Top to Bottom
//
// So the first thing we should copy is the Top and Right most
// value from source to target.
//
// +----target-- +----target--
// A | B C | A | B E |
// +------------ | step 1 +------------ |
// | D | E | F | =========> | D | E | F |
// | ------------- | -------------
// | G H | I | G H | I
// --source----- --source-----
//
// And look. The E which was in the overlapping part of the source
// is the first thing copied out of the way and we're safe to copy the rest.
//
// We assume that this pattern then applies to all ordinal directions as well
// and it appears our rules hold.
//
// We've covered all cardinal and ordinal directions... all that is left is two
// rectangles of the same size and origin... and in that case, it doesn't matter
// as nothing is moving and therefore can't be covered up or lost.
//
// Therefore, we will codify our inequalities below as determining the walk direction
// for a given source and target viewport and use the helper `GetWalkOrigin`
// to return the place that we should start walking from when the copy commences.
const auto sourceOrigin = source.Origin();
const auto targetOrigin = target.Origin();
return Viewport::WalkDir{ targetOrigin.X < sourceOrigin.X ? Viewport::XWalk::LeftToRight : Viewport::XWalk::RightToLeft,
targetOrigin.Y < sourceOrigin.Y ? Viewport::YWalk::TopToBottom : Viewport::YWalk::BottomToTop };
}
// Method Description:
// - Clips the input rectangle to our bounds. Assumes that the input rectangle
//is an exclusive rectangle.
// Arguments:
// - psr: a pointer to an exclusive rect to clip.
// Return Value:
// - true iff the clipped rectangle is valid (with a width and height both >0)
bool Viewport::TrimToViewport(_Inout_ SMALL_RECT* const psr) const noexcept
{
psr->Left = std::max(psr->Left, Left());
psr->Right = std::min(psr->Right, RightExclusive());
psr->Top = std::max(psr->Top, Top());
psr->Bottom = std::min(psr->Bottom, BottomExclusive());
return psr->Left < psr->Right && psr->Top < psr->Bottom;
}
// Method Description:
// - Translates the input SMALL_RECT out of our coordinate space, whose origin is
// at (this.Left, this.Right)
// Arguments:
// - psr: a pointer to a SMALL_RECT the translate into our coordinate space.
// Return Value:
// - <none>
void Viewport::ConvertToOrigin(_Inout_ SMALL_RECT* const psr) const noexcept
{
const short dx = Left();
const short dy = Top();
psr->Left -= dx;
psr->Right -= dx;
psr->Top -= dy;
psr->Bottom -= dy;
}
// Method Description:
// - Translates the input coordinate out of our coordinate space, whose origin is
// at (this.Left, this.Right)
// Arguments:
// - pcoord: a pointer to a coordinate the translate into our coordinate space.
// Return Value:
// - <none>
void Viewport::ConvertToOrigin(_Inout_ COORD* const pcoord) const noexcept
{
pcoord->X -= Left();
pcoord->Y -= Top();
}
// Method Description:
// - Translates the input SMALL_RECT to our coordinate space, whose origin is
// at (this.Left, this.Right)
// Arguments:
// - psr: a pointer to a SMALL_RECT the translate into our coordinate space.
// Return Value:
// - <none>
void Viewport::ConvertFromOrigin(_Inout_ SMALL_RECT* const psr) const noexcept
{
const short dx = Left();
const short dy = Top();
psr->Left += dx;
psr->Right += dx;
psr->Top += dy;
psr->Bottom += dy;
}
// Method Description:
// - Translates the input coordinate to our coordinate space, whose origin is
// at (this.Left, this.Right)
// Arguments:
// - pcoord: a pointer to a coordinate the translate into our coordinate space.
// Return Value:
// - <none>
void Viewport::ConvertFromOrigin(_Inout_ COORD* const pcoord) const noexcept
{
pcoord->X += Left();
pcoord->Y += Top();
}
// Method Description:
// - Returns an exclusive SMALL_RECT equivalent to this viewport.
// Arguments:
// - <none>
// Return Value:
// - an exclusive SMALL_RECT equivalent to this viewport.
SMALL_RECT Viewport::ToExclusive() const noexcept
{
return { Left(), Top(), RightExclusive(), BottomExclusive() };
}
// Method Description:
// - Returns an exclusive RECT equivalent to this viewport.
// Arguments:
// - <none>
// Return Value:
// - an exclusive RECT equivalent to this viewport.
RECT Viewport::ToRect() const noexcept
{
RECT r{ 0 };
r.left = Left();
r.top = Top();
r.right = RightExclusive();
r.bottom = BottomExclusive();
return r;
}
// Method Description:
// - Returns an inclusive SMALL_RECT equivalent to this viewport.
// Arguments:
// - <none>
// Return Value:
// - an inclusive SMALL_RECT equivalent to this viewport.
SMALL_RECT Viewport::ToInclusive() const noexcept
{
return { Left(), Top(), RightInclusive(), BottomInclusive() };
}
// Method Description:
// - Returns a new viewport representing this viewport at the origin.
// For example:
// this = {6, 5, 11, 11} (w, h = 5, 6)
// result = {0, 0, 5, 6} (w, h = 5, 6)
// Arguments:
// - <none>
// Return Value:
// - a new viewport with the same dimensions as this viewport with top, left = 0, 0
Viewport Viewport::ToOrigin() const noexcept
{
Viewport returnVal = *this;
ConvertToOrigin(&returnVal._sr);
return returnVal;
}
// Method Description:
// - Translates another viewport to this viewport's coordinate space.
// For example:
// this = {5, 6, 7, 8} (w,h = 1, 1)
// other = {6, 5, 11, 11} (w, h = 5, 6)
// result = {1, -1, 6, 5} (w, h = 5, 6)
// Arguments:
// - other: the viewport to convert to this coordinate space
// Return Value:
// - the input viewport in a the coordinate space with origin at (this.Top, this.Left)
[[nodiscard]] Viewport Viewport::ConvertToOrigin(const Viewport& other) const noexcept
{
Viewport returnVal = other;
ConvertToOrigin(&returnVal._sr);
return returnVal;
}
// Method Description:
// - Translates another viewport out of this viewport's coordinate space.
// For example:
// this = {5, 6, 7, 8} (w,h = 1, 1)
// other = {0, 0, 5, 6} (w, h = 5, 6)
// result = {5, 6, 10, 12} (w, h = 5, 6)
// Arguments:
// - other: the viewport to convert out of this coordinate space
// Return Value:
// - the input viewport in a the coordinate space with origin at (0, 0)
[[nodiscard]] Viewport Viewport::ConvertFromOrigin(const Viewport& other) const noexcept
{
Viewport returnVal = other;
ConvertFromOrigin(&returnVal._sr);
return returnVal;
}
// Function Description:
// - Translates a given Viewport by the specified coord amount. Does the
// addition with safemath.
// Arguments:
// - original: The initial viewport to translate. Is unmodified by this operation.
// - delta: The amount to translate the original rect by, in both the x and y coordinates.
// Return Value:
// - The offset viewport by the given delta.
// - NOTE: Throws on safe math failure.
[[nodiscard]] Viewport Viewport::Offset(const Viewport& original, const COORD delta)
{
// If there's no delta, do nothing.
if (delta.X == 0 && delta.Y == 0)
{
return original;
}
SHORT newTop = original._sr.Top;
SHORT newLeft = original._sr.Left;
SHORT newRight = original._sr.Right;
SHORT newBottom = original._sr.Bottom;
THROW_IF_FAILED(ShortAdd(newLeft, delta.X, &newLeft));
THROW_IF_FAILED(ShortAdd(newRight, delta.X, &newRight));
THROW_IF_FAILED(ShortAdd(newTop, delta.Y, &newTop));
THROW_IF_FAILED(ShortAdd(newBottom, delta.Y, &newBottom));
return Viewport({ newLeft, newTop, newRight, newBottom });
}
// Function Description:
// - Returns a viewport created from the union of both the parameter viewports.
// The result extends from the leftmost extent of either rect to the
// rightmost extent of either rect, and from the lowest top value to the
// highest bottom value, and everything in between.
// Arguments:
// - lhs: one of the viewports to or together
// - rhs: the other viewport to or together
// Return Value:
// - a Viewport representing the union of the other two viewports.
[[nodiscard]] Viewport Viewport::Union(const Viewport& lhs, const Viewport& rhs) noexcept
{
const auto leftValid = lhs.IsValid();
const auto rightValid = rhs.IsValid();
// If neither are valid, return empty.
if (!leftValid && !rightValid)
{
return Viewport::Empty();
}
// If left isn't valid, then return just the right.
else if (!leftValid)
{
return rhs;
}
// If right isn't valid, then return just the left.
else if (!rightValid)
{
return lhs;
}
// Otherwise, everything is valid. Find the actual union.
else
{
const auto left = std::min(lhs.Left(), rhs.Left());
const auto top = std::min(lhs.Top(), rhs.Top());
const auto right = std::max(lhs.RightInclusive(), rhs.RightInclusive());
const auto bottom = std::max(lhs.BottomInclusive(), rhs.BottomInclusive());
return Viewport({ left, top, right, bottom });
}
}
// Function Description:
// - Creates a viewport from the intersection fo both the parameter viewports.
// The result will be the smallest area that fits within both rectangles.
// Arguments:
// - lhs: one of the viewports to intersect
// - rhs: the other viepwort to intersect
// Return Value:
// - a Viewport representing the intersection of the other two, or an empty viewport if there's no intersection.
[[nodiscard]] Viewport Viewport::Intersect(const Viewport& lhs, const Viewport& rhs) noexcept
{
const auto left = std::max(lhs.Left(), rhs.Left());
const auto top = std::max(lhs.Top(), rhs.Top());
const auto right = std::min(lhs.RightInclusive(), rhs.RightInclusive());
const auto bottom = std::min(lhs.BottomInclusive(), rhs.BottomInclusive());
const Viewport intersection({ left, top, right, bottom });
// What we calculated with min/max might not actually represent a valid viewport that has area.
// If we calculated something that is nonsense (invalid), then just return the empty viewport.
if (!intersection.IsValid())
{
return Viewport::Empty();
}
else
{
// If it was valid, give back whatever we created.
return intersection;
}
}
// Routine Description:
// - Returns a list of Viewports representing the area from the `original` Viewport that was NOT a part of
// the given `removeMe` Viewport. It can require multiple Viewports to represent the remaining
// area as a "region".
// Arguments:
// - original - The overall viewport to start from.
// - removeMe - The space that should be taken out of the main Viewport.
// Return Value:
// - Array of 4 Viewports representing non-overlapping segments of the remaining area
// that was covered by `main` before the regional area of `removeMe` was taken out.
// - You must check that each viewport .IsValid() before using it.
[[nodiscard]] SomeViewports Viewport::Subtract(const Viewport& original, const Viewport& removeMe) noexcept
{
SomeViewports result;
// We could have up to four rectangles describing the area resulting when you take removeMe out of main.
// Find the intersection of the two so we know which bits of removeMe are actually applicable
// to the original rectangle for subtraction purposes.
const auto intersection = Viewport::Intersect(original, removeMe);
// If there's no intersection, there's nothing to remove.
if (!intersection.IsValid())
{
// Just put the original rectangle into the results and return early.
result.viewports.at(result.used++) = original;
}
// If the original rectangle matches the intersection, there is nothing to return.
else if (original != intersection)
{
// Generate our potential four viewports that represent the region of the original that falls outside of the remove area.
// We will bias toward generating wide rectangles over tall rectangles (if possible) so that optimizations that apply
// to manipulating an entire row at once can be realized by other parts of the console code. (i.e. Run Length Encoding)
// In the following examples, the found remaining regions are represented by:
// T = Top B = Bottom L = Left R = Right
//
// 4 Sides but Identical:
// |---------original---------| |---------original---------|
// | | | |
// | | | |
// | | | |
// | | ======> | intersect | ======> early return of nothing
// | | | |
// | | | |
// | | | |
// |---------removeMe---------| |--------------------------|
//
// 4 Sides:
// |---------original---------| |---------original---------| |--------------------------|
// | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT|
// | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT|
// | |---------| | | |---------| | |LLLLLLLL|---------|RRRRRRR|
// | |removeMe | | ======> | |intersect| | ======> |LLLLLLLL| |RRRRRRR|
// | |---------| | | |---------| | |LLLLLLLL|---------|RRRRRRR|
// | | | | |BBBBBBBBBBBBBBBBBBBBBBBBBB|
// | | | | |BBBBBBBBBBBBBBBBBBBBBBBBBB|
// |--------------------------| |--------------------------| |--------------------------|
//
// 3 Sides:
// |---------original---------| |---------original---------| |--------------------------|
// | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT|
// | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT|
// | |--------------------| | |-----------------| |LLLLLLLL|-----------------|
// | |removeMe | ======> | |intersect | ======> |LLLLLLLL| |
// | |--------------------| | |-----------------| |LLLLLLLL|-----------------|
// | | | | |BBBBBBBBBBBBBBBBBBBBBBBBBB|
// | | | | |BBBBBBBBBBBBBBBBBBBBBBBBBB|
// |--------------------------| |--------------------------| |--------------------------|
//
// 2 Sides:
// |---------original---------| |---------original---------| |--------------------------|
// | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT|
// | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT|
// | |--------------------| | |-----------------| |LLLLLLLL|-----------------|
// | |removeMe | ======> | |intersect | ======> |LLLLLLLL| |
// | | | | | | |LLLLLLLL| |
// | | | | | | |LLLLLLLL| |
// | | | | | | |LLLLLLLL| |
// |--------| | |--------------------------| |--------------------------|
// | |
// |--------------------|
//
// 1 Side:
// |---------original---------| |---------original---------| |--------------------------|
// | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT|
// | | | | |TTTTTTTTTTTTTTTTTTTTTTTTTT|
// |-----------------------------| |--------------------------| |--------------------------|
// | removeMe | ======> | intersect | ======> | |
// | | | | | |
// | | | | | |
// | | | | | |
// | | |--------------------------| |--------------------------|
// | |
// |-----------------------------|
//
// 0 Sides:
// |---------original---------| |---------original---------|
// | | | |
// | | | |
// | | | |
// | | ======> | | ======> early return of Original
// | | | |
// | | | |
// | | | |
// |--------------------------| |--------------------------|
//
//
// |---------------|
// | removeMe |
// |---------------|
// We generate these rectangles by the original and intersection points, but some of them might be empty when the intersection
// lines up with the edge of the original. That's OK. That just means that the subtraction didn't leave anything behind.
// We will filter those out below when adding them to the result.
const auto top = Viewport({ original.Left(), original.Top(), original.RightInclusive(), intersection.Top() - 1 });
const auto bottom = Viewport({ original.Left(), intersection.BottomExclusive(), original.RightInclusive(), original.BottomInclusive() });
const auto left = Viewport({ original.Left(), intersection.Top(), intersection.Left() - 1, intersection.BottomInclusive() });
const auto right = Viewport({ intersection.RightExclusive(), intersection.Top(), original.RightInclusive(), intersection.BottomInclusive() });
if (top.IsValid())
{
result.viewports.at(result.used++) = top;
}
if (bottom.IsValid())
{
result.viewports.at(result.used++) = bottom;
}
if (left.IsValid())
{
result.viewports.at(result.used++) = left;
}
if (right.IsValid())
{
result.viewports.at(result.used++) = right;
}
}
return result;
}
// Method Description:
// - Returns true if the rectangle described by this Viewport has internal space
// - i.e. it has a positive, non-zero height and width.
bool Viewport::IsValid() const noexcept
{
return Height() > 0 && Width() > 0;
}