terminal/src/host/popup.cpp

318 lines
11 KiB
C++
Raw Normal View History

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "popup.h"
#include "_output.h"
#include "output.h"
#include "dbcs.h"
#include "srvinit.h"
#include "stream.h"
#include "resource.h"
#include "utils.hpp"
#include "../interactivity/inc/ServiceLocator.hpp"
#pragma hdrstop
using namespace Microsoft::Console::Types;
using Microsoft::Console::Interactivity::ServiceLocator;
// Routine Description:
// - Creates an object representing an interactive popup overlay during cooked mode command line editing.
// - NOTE: Modifies global popup count (and adjusts cursor visibility as appropriate.)
// Arguments:
// - screenInfo - Reference to screen on which the popup should be drawn/overlayed.
// - proposedSize - Suggested size of the popup. May be adjusted based on screen size.
Popup::Popup(SCREEN_INFORMATION& screenInfo, const COORD proposedSize) :
_screenInfo(screenInfo),
_userInputFunction(&Popup::_getUserInputInternal)
{
Improve the legacy color conversions (#6358) This PR provides a faster algorithm for converting 8-bit and 24-bit colors into the 4-bit legacy values that are required by the Win32 console APIs. It also fixes areas of the code that were incorrectly using a simple 16-color conversion that didn't handle 8-bit and 24-bit values. The faster conversion algorithm should be an improvement for issues #783 and #3950. One of the main points of this PR was to fix the `ReadConsoleOutputAttribute` API, which was using a simplified legacy color conversion (the original `TextAttribute:GetLegacyAttributes` method), which could only handle values from the 16-color table. RGB values, and colors from the 256-color table, would be mapped to completely nonsensical values. This API has now been updated to use the more correct `Settings::GenerateLegacyAttributes` method. But there were also a couple of other places in the code that were using `GetLegacyAttributes` when they really had no reason to be working with legacy attributes at all. This could result in colors being downgraded to 4-bit values (often badly, as explained above), when the code was already perfectly capable of displaying the full 24-bits. This included the fill colors in the IME composer (in `ConsoleImeInfo`), and the construction of the highlighting colors in the color search/selection handler (`Selection::_HandleColorSelection`). I also got rid of some legacy attribute code in the `Popup` class, which was originally intended to update colors below the popup when the settings changed, but actually caused more problems than it solved. The other major goal of this PR was to improve the performance of the `GenerateLegacyAttributes` method, since the existing implementation could be quite slow when dealing with RGB values. The simple cases are handled much the same as they were before. For an `IsDefault` color, we get the default index from the `Settings::_wFillAttribute` field. For an `IsIndex16` color, the index can just be returned as is. For an `IsRgb` color, the RGB components are compressed down to 8 bits (3 red, 3 green, 2 blue), simply by dropping the least significant bits. This 8-bit value is then used to lookup a representative 16-color value from a hard-coded table. An `IsIndex256` color is also converted with a lookup table, just using the existing 8-bit index. The RGB mapping table was calculated by taking each compressed 8-bit color, and picking a entry from the _Campbell_ palette that best approximated that color. This was done by looking at a range of 24-bit colors that mapped to the 8-bit value, finding the best _Campbell_ match for each of them (using a [CIEDE2000] color difference calculation), and then the most common match became the index that the 8-bit value would map to. The 256-color table was just a simpler version of this process. For each entry in the table, we take the default RGB palette value, and find it's closest match in the _Campbell_ palette. Because these tables are hard-coded, the results won't adjust to changes in the palette. However, they should still produce reasonable results for palettes that follow the standard ANSI color range. And since they're only a very loose approximation of the colors anyway, the exact value really isn't that important. That said, I have tried to make sure that if you take an RGB value for a particular index in a reasonable color scheme, then the legacy color mapped from that value should ideally match the same index. This will never be possible for all color schemes, but I have tweaked a few of the table entries to improve the results for some of the common schemes. One other point worth making regarding the hard-coded tables: even if we wanted to take the active palette into account, that wouldn't actually be possible over a conpty connection, because we can't easily know what color scheme the client application is using. At least this way the results in conhost are guaranteed to be the same as in the Windows Terminal. [CIEDE2000]: https://en.wikipedia.org/wiki/Color_difference#CIEDE2000 ## Validation Steps Performed This code still passes the `TextAttributeTests` that check the basic `GetLegacyAttribute` behaviour and verify the all legacy attributes roundtrip correctly. However, some of the values in the `RgbColorTests` had to be updated, since we're now intentionally returning different values as a result of the changes to the RGB conversion algorithm. I haven't added additional unit tests, but I have done a lot of manual testing to see how well the new algorithm works with a range of colors and a variety of different color schemes. It's not perfect in every situation, but I think it works well enough for the purpose it serves. I've also confirmed that the issues reported in #5940 and #6247 are now fixed by these changes. Closes #5940 Closes #6247
2020-06-08 21:05:06 +02:00
_attributes = screenInfo.GetPopupAttributes();
const COORD size = _CalculateSize(screenInfo, proposedSize);
const COORD origin = _CalculateOrigin(screenInfo, size);
_region.Left = origin.X;
_region.Top = origin.Y;
_region.Right = gsl::narrow<SHORT>(origin.X + size.X - 1i16);
_region.Bottom = gsl::narrow<SHORT>(origin.Y + size.Y - 1i16);
_oldScreenSize = screenInfo.GetBufferSize().Dimensions();
SMALL_RECT TargetRect;
TargetRect.Left = 0;
TargetRect.Top = _region.Top;
TargetRect.Right = _oldScreenSize.X - 1;
TargetRect.Bottom = _region.Bottom;
// copy the data into the backup buffer
_oldContents = std::move(screenInfo.ReadRect(Viewport::FromInclusive(TargetRect)));
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto countWas = gci.PopupCount.fetch_add(1ui16);
if (0 == countWas)
{
// If this is the first popup to be shown, stop the cursor from appearing/blinking
screenInfo.GetTextBuffer().GetCursor().SetIsPopupShown(true);
}
}
// Routine Description:
// - Cleans up a popup object
// - NOTE: Modifies global popup count (and adjusts cursor visibility as appropriate.)
Popup::~Popup()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto countWas = gci.PopupCount.fetch_sub(1i16);
if (1 == countWas)
{
// Notify we're done showing popups.
gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().SetIsPopupShown(false);
}
}
void Popup::Draw()
{
_DrawBorder();
_DrawContent();
}
// Routine Description:
// - Draws the outlines of the popup area in the screen buffer
void Popup::_DrawBorder()
{
// fill attributes of top line
COORD WriteCoord;
WriteCoord.X = _region.Left;
WriteCoord.Y = _region.Top;
_screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord);
// draw upper left corner
_screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_DOWN_AND_RIGHT, 1), WriteCoord);
// draw upper bar
WriteCoord.X += 1;
_screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_HORIZONTAL, Width()), WriteCoord);
// draw upper right corner
WriteCoord.X = _region.Right;
_screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_DOWN_AND_LEFT, 1), WriteCoord);
for (SHORT i = 0; i < Height(); i++)
{
WriteCoord.Y += 1;
WriteCoord.X = _region.Left;
// fill attributes
_screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord);
_screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_VERTICAL, 1), WriteCoord);
WriteCoord.X = _region.Right;
_screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_VERTICAL, 1), WriteCoord);
}
// Draw bottom line.
// Fill attributes of top line.
WriteCoord.X = _region.Left;
WriteCoord.Y = _region.Bottom;
_screenInfo.Write(OutputCellIterator(_attributes, Width() + 2), WriteCoord);
// Draw bottom left corner.
WriteCoord.X = _region.Left;
_screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_UP_AND_RIGHT, 1), WriteCoord);
// Draw lower bar.
WriteCoord.X += 1;
_screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_HORIZONTAL, Width()), WriteCoord);
// draw lower right corner
WriteCoord.X = _region.Right;
_screenInfo.Write(OutputCellIterator(UNICODE_BOX_DRAW_LIGHT_UP_AND_LEFT, 1), WriteCoord);
}
// Routine Description:
// - Draws prompt information in the popup area to tell the user what to enter.
// Arguments:
// - id - Resource ID for string to display to user
void Popup::_DrawPrompt(const UINT id)
{
std::wstring text = _LoadString(id);
// Draw empty popup.
COORD WriteCoord;
WriteCoord.X = _region.Left + 1i16;
WriteCoord.Y = _region.Top + 1i16;
size_t lStringLength = Width();
for (SHORT i = 0; i < Height(); i++)
{
const OutputCellIterator it(UNICODE_SPACE, _attributes, lStringLength);
const auto done = _screenInfo.Write(it, WriteCoord);
lStringLength = done.GetCellDistance(it);
WriteCoord.Y += 1;
}
WriteCoord.X = _region.Left + 1i16;
WriteCoord.Y = _region.Top + 1i16;
// write prompt to screen
lStringLength = text.size();
if (lStringLength > (ULONG)Width())
{
text = text.substr(0, Width());
}
size_t used;
LOG_IF_FAILED(ServiceLocator::LocateGlobals().api.WriteConsoleOutputCharacterWImpl(_screenInfo,
text,
WriteCoord,
used));
}
// Routine Description:
// - Cleans up a popup by restoring the stored buffer information to the region of
// the screen that the popup was covering and frees resources.
void Popup::End()
{
// restore previous contents to screen
SMALL_RECT SourceRect;
SourceRect.Left = 0i16;
SourceRect.Top = _region.Top;
SourceRect.Right = _oldScreenSize.X - 1i16;
SourceRect.Bottom = _region.Bottom;
const auto sourceViewport = Viewport::FromInclusive(SourceRect);
_screenInfo.WriteRect(_oldContents, sourceViewport.Origin());
}
// Routine Description:
// - Helper to calculate the size of the popup.
// Arguments:
// - screenInfo - Screen buffer we will be drawing into
// - proposedSize - The suggested size of the popup that may need to be adjusted to fit
// Return Value:
// - Coordinate size that the popup should consume in the screen buffer
COORD Popup::_CalculateSize(const SCREEN_INFORMATION& screenInfo, const COORD proposedSize)
{
// determine popup dimensions
COORD size = proposedSize;
size.X += 2; // add borders
size.Y += 2; // add borders
const COORD viewportSize = screenInfo.GetViewport().Dimensions();
size.X = std::min(size.X, viewportSize.X);
size.Y = std::min(size.Y, viewportSize.Y);
// make sure there's enough room for the popup borders
THROW_HR_IF(E_NOT_SUFFICIENT_BUFFER, size.X < 2 || size.Y < 2);
return size;
}
// Routine Description:
// - Helper to calculate the origin point (within the screen buffer) for the popup
// Arguments:
// - screenInfo - Screen buffer we will be drawing into
// - size - The size that the popup will consume
// Return Value:
// - Coordinate position of the origin point of the popup
COORD Popup::_CalculateOrigin(const SCREEN_INFORMATION& screenInfo, const COORD size)
{
const auto viewport = screenInfo.GetViewport();
// determine origin. center popup on window
COORD origin;
origin.X = gsl::narrow<SHORT>((viewport.Width() - size.X) / 2 + viewport.Left());
origin.Y = gsl::narrow<SHORT>((viewport.Height() - size.Y) / 2 + viewport.Top());
return origin;
}
// Routine Description:
// - Helper to return the width of the popup in columns
// Return Value:
// - Width of popup inside attached screen buffer.
SHORT Popup::Width() const noexcept
{
return _region.Right - _region.Left - 1i16;
}
// Routine Description:
// - Helper to return the height of the popup in columns
// Return Value:
// - Height of popup inside attached screen buffer.
SHORT Popup::Height() const noexcept
{
return _region.Bottom - _region.Top - 1i16;
}
// Routine Description:
// - Helper to get the position on top of some types of popup dialogs where
// we should overlay the cursor for user input.
// Return Value:
// - Coordinate location on the popup where the cursor should be placed.
COORD Popup::GetCursorPosition() const noexcept
{
COORD CursorPosition;
CursorPosition.X = _region.Right - static_cast<SHORT>(MINIMUM_COMMAND_PROMPT_SIZE);
CursorPosition.Y = _region.Top + 1i16;
return CursorPosition;
}
// Routine Description:
// - changes the function used to gather user input. for allowing custom input during unit tests only
// Arguments:
// - function - function to use to fetch input
void Popup::SetUserInputFunction(UserInputFunction function) noexcept
{
_userInputFunction = function;
}
// Routine Description:
// - gets a single char input from the user
// Arguments:
// - cookedReadData - cookedReadData for this popup operation
// - popupKey - on completion, will be true if key was a popup key
// - wch - on completion, the char read from the user
// Return Value:
// - relevant NTSTATUS
[[nodiscard]] NTSTATUS Popup::_getUserInput(COOKED_READ_DATA& cookedReadData, bool& popupKey, DWORD& modifiers, wchar_t& wch) noexcept
{
return _userInputFunction(cookedReadData, popupKey, modifiers, wch);
}
// Routine Description:
// - gets a single char input from the user using the InputBuffer
// Arguments:
// - cookedReadData - cookedReadData for this popup operation
// - popupKey - on completion, will be true if key was a popup key
// - wch - on completion, the char read from the user
// Return Value:
// - relevant NTSTATUS
[[nodiscard]] NTSTATUS Popup::_getUserInputInternal(COOKED_READ_DATA& cookedReadData,
bool& popupKey,
DWORD& modifiers,
wchar_t& wch) noexcept
{
InputBuffer* const pInputBuffer = cookedReadData.GetInputBuffer();
NTSTATUS Status = GetChar(pInputBuffer,
&wch,
true,
nullptr,
&popupKey,
&modifiers);
if (!NT_SUCCESS(Status) && Status != CONSOLE_STATUS_WAIT)
{
cookedReadData.BytesRead() = 0;
}
return Status;
}