terminal/src/host/conimeinfo.cpp

508 lines
20 KiB
C++
Raw Normal View History

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "conimeinfo.h"
#include "conareainfo.h"
#include "_output.h"
#include "dbcs.h"
#include "../interactivity/inc/ServiceLocator.hpp"
#include "../types/inc/GlyphWidth.hpp"
#include "../types/inc/Utf16Parser.hpp"
// Attributes flags:
#define COMMON_LVB_GRID_SINGLEFLAG 0x2000 // DBCS: Grid attribute: use for ime cursor.
using Microsoft::Console::Interactivity::ServiceLocator;
ConsoleImeInfo::ConsoleImeInfo() :
_isSavedCursorVisible(false)
{
}
// Routine Description:
// - Copies default attribute (color) data from the active screen buffer into the conversion area buffers
void ConsoleImeInfo::RefreshAreaAttributes()
{
const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
const auto attributes = gci.GetActiveOutputBuffer().GetAttributes();
for (auto& area : ConvAreaCompStr)
{
area.SetAttributes(attributes);
}
}
// Routine Description:
// - Takes the internally held composition message data from the last WriteCompMessage call
// and attempts to redraw it on the screen which will account for changes in viewport dimensions
void ConsoleImeInfo::RedrawCompMessage()
{
if (!_text.empty())
{
ClearAllAreas();
_WriteUndeterminedChars(_text, _attributes, _colorArray);
}
}
// Routine Description:
// - Writes an undetermined composition message to the screen including the text
// and color and cursor positioning attribute data so the user can walk through
// what they're proposing to insert into the buffer.
// Arguments:
// - text - The actual text of what the user would like to insert (UTF-16)
// - attributes - Encoded attributes including the cursor position and the color index (to the array)
// - colorArray - An array of colors to use for the text
void ConsoleImeInfo::WriteCompMessage(const std::wstring_view text,
const gsl::span<const BYTE> attributes,
const gsl::span<const WORD> colorArray)
{
ClearAllAreas();
// MSFT:29219348 only hide the cursor after the IME produces a string.
// See notes in convarea.cpp ImeStartComposition().
SaveCursorVisibility();
// Save copies of the composition message in case we need to redraw it as things scroll/resize
_text = text;
_attributes.assign(attributes.begin(), attributes.end());
_colorArray.assign(colorArray.begin(), colorArray.end());
_WriteUndeterminedChars(text, attributes, colorArray);
}
// Routine Description:
// - Writes the final result into the screen buffer through the input queue
// as if the user had inputted it (if their keyboard was able to)
// Arguments:
// - text - The actual text of what the user would like to insert (UTF-16)
void ConsoleImeInfo::WriteResultMessage(const std::wstring_view text)
{
ClearAllAreas();
_InsertConvertedString(text);
_ClearComposition();
}
// Routine Description:
// - Clears internally cached composition data from the last WriteCompMessage call.
void ConsoleImeInfo::_ClearComposition()
{
_text.clear();
_attributes.clear();
_colorArray.clear();
}
// Routine Description:
// - Clears out all conversion areas
void ConsoleImeInfo::ClearAllAreas()
{
for (auto& area : ConvAreaCompStr)
{
if (!area.IsHidden())
{
area.ClearArea();
}
}
// Also clear internal buffer of string data.
_ClearComposition();
}
// Routine Description:
// - Resizes all conversion areas to the new dimensions
// Arguments:
// - newSize - New size for conversion areas
// Return Value:
// - S_OK or appropriate failure HRESULT.
[[nodiscard]] HRESULT ConsoleImeInfo::ResizeAllAreas(const COORD newSize)
{
for (auto& area : ConvAreaCompStr)
{
if (!area.IsHidden())
{
area.SetHidden(true);
area.Paint();
}
RETURN_IF_FAILED(area.Resize(newSize));
}
return S_OK;
}
// Routine Description:
// - Adds another conversion area to the current list of conversion areas (lines) available for IME candidate text
// Arguments:
// - <none>
// Return Value:
// - Status successful or appropriate HRESULT response.
[[nodiscard]] HRESULT ConsoleImeInfo::_AddConversionArea()
{
const CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
COORD bufferSize = gci.GetActiveOutputBuffer().GetBufferSize().Dimensions();
bufferSize.Y = 1;
const COORD windowSize = gci.GetActiveOutputBuffer().GetViewport().Dimensions();
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
const TextAttribute fill = gci.GetActiveOutputBuffer().GetAttributes();
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
const TextAttribute popupFill = gci.GetActiveOutputBuffer().GetPopupAttributes();
const FontInfo& fontInfo = gci.GetActiveOutputBuffer().GetCurrentFont();
try
{
ConvAreaCompStr.emplace_back(bufferSize,
windowSize,
fill,
popupFill,
fontInfo);
}
CATCH_RETURN();
RefreshAreaAttributes();
return S_OK;
}
// Routine Description:
// - Helper method to decode the cursor and color position out of the encoded attributes
// and color array and return it in the TextAttribute structure format
// Arguments:
// - pos - Character position in the string (and matching encoded attributes array)
// - attributes - Encoded attributes holding cursor and color array position
// - colorArray - Colors to choose from
// Return Value:
// - TextAttribute object with color and cursor and line drawing data.
TextAttribute ConsoleImeInfo::s_RetrieveAttributeAt(const size_t pos,
const gsl::span<const BYTE> attributes,
const gsl::span<const WORD> colorArray)
{
// Encoded attribute is the shorthand information passed from the IME
// that contains a cursor position packed in along with which color in the
// given array should apply to the text.
auto encodedAttribute = attributes[pos];
// Legacy attribute is in the color/line format that is understood for drawing
// We use the lower 3 bits (0-7) from the encoded attribute as the array index to start
// creating our legacy attribute.
WORD legacyAttribute = colorArray[encodedAttribute & (CONIME_ATTRCOLOR_SIZE - 1)];
if (WI_IsFlagSet(encodedAttribute, CONIME_CURSOR_RIGHT))
{
WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_SINGLEFLAG);
WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_RVERTICAL);
}
else if (WI_IsFlagSet(encodedAttribute, CONIME_CURSOR_LEFT))
{
WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_SINGLEFLAG);
WI_SetFlag(legacyAttribute, COMMON_LVB_GRID_LVERTICAL);
}
return TextAttribute(legacyAttribute);
}
// Routine Description:
// - Converts IME-formatted information into OutputCells to determine what can fit into each
// displayable cell inside the console output buffer.
// Arguments:
// - text - Text data provided by the IME
// - attributes - Encoded color and cursor position data provided by the IME
// - colorArray - Array of color values provided by the IME.
// Return Value:
// - Vector of OutputCells where each one represents one cell of the output buffer.
std::vector<OutputCell> ConsoleImeInfo::s_ConvertToCells(const std::wstring_view text,
const gsl::span<const BYTE> attributes,
const gsl::span<const WORD> colorArray)
{
std::vector<OutputCell> cells;
// - Convert incoming wchar_t stream into UTF-16 units.
const auto glyphs = Utf16Parser::Parse(text);
// - Walk through all of the grouped up text, match up the correct attribute to it, and make a new cell.
size_t attributesUsed = 0;
for (const auto& parsedGlyph : glyphs)
{
const std::wstring_view glyph{ parsedGlyph.data(), parsedGlyph.size() };
// Collect up attributes that apply to this glyph range.
auto drawingAttr = s_RetrieveAttributeAt(attributesUsed, attributes, colorArray);
attributesUsed++;
// The IME gave us an attribute for every glyph position in a surrogate pair.
// But the only important information will be the cursor position.
// Check all additional attributes to see if the cursor resides on top of them.
for (size_t i = 1; i < glyph.size(); i++)
{
TextAttribute additionalAttr = s_RetrieveAttributeAt(attributesUsed, attributes, colorArray);
attributesUsed++;
if (additionalAttr.IsLeftVerticalDisplayed())
{
drawingAttr.SetLeftVerticalDisplayed(true);
}
if (additionalAttr.IsRightVerticalDisplayed())
{
drawingAttr.SetRightVerticalDisplayed(true);
}
}
// We have to determine if the glyph range is 1 column or two.
// If it's full width, it's two, and we need to make sure we don't draw the cursor
// right down the middle of the character.
// Otherwise it's one column and we'll push it in with the default empty DbcsAttribute.
DbcsAttribute dbcsAttr;
if (IsGlyphFullWidth(glyph))
{
auto leftHalfAttr = drawingAttr;
auto rightHalfAttr = drawingAttr;
// Don't draw lines in the middle of full width glyphs.
// If we need a right vertical, don't apply it to the left side of the character
if (leftHalfAttr.IsRightVerticalDisplayed())
{
leftHalfAttr.SetRightVerticalDisplayed(false);
}
dbcsAttr.SetLeading();
cells.emplace_back(glyph, dbcsAttr, leftHalfAttr);
dbcsAttr.SetTrailing();
// If we need a left vertical, don't apply it to the right side of the character
if (rightHalfAttr.IsLeftVerticalDisplayed())
{
rightHalfAttr.SetLeftVerticalDisplayed(false);
}
cells.emplace_back(glyph, dbcsAttr, rightHalfAttr);
}
else
{
cells.emplace_back(glyph, dbcsAttr, drawingAttr);
}
}
return cells;
}
// Routine Description:
// - Walks through the cells given and attempts to fill a conversion area line with as much data as can fit.
// - Each conversion area represents one line of the display starting at the cursor position filling to the right edge
// of the display.
// - The first conversion area should be placed from the screen buffer's current cursor position to the right
// edge of the viewport.
// - All subsequent areas should use one entire line of the viewport.
// Arguments:
// - begin - Beginning position in OutputCells for iteration
// - end - Ending position in OutputCells for iteration
// - pos - Reference to the coordinate position in the viewport that this conversion area will occupy.
// - Updated to set up the next conversion area down a line (and to the left viewport edge)
// - view - The rectangle representing the viewable area of the screen right now to let us know how many cells can fit.
// - screenInfo - A reference to the screen information we will use for accessibility notifications
// Return Value:
// - Updated begin position for the next call. It will normally be >begin and <= end.
// However, if text couldn't fit in our line (full-width character starting at the very last cell)
// then we will give back the same begin and update the position for the next call to try again.
// If the viewport is deemed too small, we'll skip past it and advance begin past the entire full-width character.
std::vector<OutputCell>::const_iterator ConsoleImeInfo::_WriteConversionArea(const std::vector<OutputCell>::const_iterator begin,
const std::vector<OutputCell>::const_iterator end,
COORD& pos,
const Microsoft::Console::Types::Viewport view,
SCREEN_INFORMATION& screenInfo)
{
// The position in the viewport where we will start inserting cells for this conversion area
// NOTE: We might exit early if there's not enough space to fit here, so we take a copy of
// the original and increment it up front.
const auto insertionPos = pos;
// Advance the cursor position to set up the next call for success (insert the next conversion area
// at the beginning of the following line)
pos.X = view.Left();
pos.Y++;
// The index of the last column in the viewport. (view is inclusive)
const auto finalViewColumn = view.RightInclusive();
// The maximum number of cells we can insert into a line.
const auto lineWidth = finalViewColumn - insertionPos.X + 1; // +1 because view was inclusive
// The iterator to the beginning position to form our line
const auto lineBegin = begin;
// The total number of cells we could insert.
const auto size = end - begin;
FAIL_FAST_IF(size <= 0); // It's a programming error to have <= 0 cells to insert.
// The end is the smaller of the remaining number of cells or the amount of line cells we can write before
// hitting the right edge of the viewport
auto lineEnd = lineBegin + std::min(size, (ptrdiff_t)lineWidth);
// We must attempt to compensate for ending on a leading byte. We can't split a full-width character across lines.
// As such, if the last item is a leading byte, back the end up by one.
FAIL_FAST_IF(lineEnd <= lineBegin); // We should have at least 1 space we can back up.
// Get the last cell in the run and if it's a leading byte, move the end position back one so we don't
// try to insert it.
const auto lastCell = lineEnd - 1;
if (lastCell->DbcsAttr().IsLeading())
{
lineEnd--;
}
// Copy out the substring into a vector.
const std::vector<OutputCell> lineVec(lineBegin, lineEnd);
// Add a conversion area to the internal state to hold this line.
THROW_IF_FAILED(_AddConversionArea());
// Get the added conversion area.
auto& area = ConvAreaCompStr.back();
// Write our text into the conversion area.
area.WriteText(lineVec, insertionPos.X);
// Set the viewport and positioning parameters for the conversion area to describe to the renderer
// the appropriate location to overlay this conversion area on top of the main screen buffer inside the viewport.
const SMALL_RECT region{ insertionPos.X, 0, gsl::narrow<SHORT>(insertionPos.X + lineVec.size() - 1), 0 };
area.SetWindowInfo(region);
area.SetViewPos({ 0 - view.Left(), insertionPos.Y - view.Top() });
// Make it visible and paint it.
area.SetHidden(false);
area.Paint();
// Notify accessibility that we have updated the text in this display region within the viewport.
screenInfo.NotifyAccessibilityEventing(insertionPos.X, insertionPos.Y, gsl::narrow<SHORT>(insertionPos.X + lineVec.size() - 1), insertionPos.Y);
// Hand back the iterator representing the end of what we used to be fed into the beginning of the next call.
return lineEnd;
}
// Routine Description:
// - Takes information from the IME message to write the "undetermined" text to the
// conversion area overlays on the screen.
// - The "undetermined" text represents the word or phrase that the user is currently building
// using the IME. They haven't "determined" what they want yet, so it's "undetermined" right now.
// Arguments:
// - text - View into the text characters provided by the IME.
// - attributes - Attributes specifying which color and cursor positioning information should apply to
// each text character. This view must be the same size as the text view.
// - colorArray - 8 colors to be used to format the text for display
void ConsoleImeInfo::_WriteUndeterminedChars(const std::wstring_view text,
const gsl::span<const BYTE> attributes,
const gsl::span<const WORD> colorArray)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
SCREEN_INFORMATION& screenInfo = gci.GetActiveOutputBuffer();
// Ensure cursor is visible for prompt line
screenInfo.MakeCurrentCursorVisible();
// Clear out existing conversion areas.
ConvAreaCompStr.clear();
// If the text length and attribute length don't match,
// it's a programming error on our part. We control the sizes here.
FAIL_FAST_IF(text.size() != attributes.size());
// If we have no text, return. We've already cleared above.
if (text.empty())
{
return;
}
// Convert data-to-be-stored into OutputCells.
const auto cells = s_ConvertToCells(text, attributes, colorArray);
// Get some starting position information of where to place the conversion areas on top of the existing
// screen buffer and viewport positioning.
// Each conversion area write will adjust these to set up any subsequent calls to go onto the next line.
auto pos = screenInfo.GetTextBuffer().GetCursor().GetPosition();
Add support for double-width/double-height lines in conhost (#8664) This PR adds support for the VT line rendition attributes, which allow for double-width and double-height line renditions. These renditions are enabled with the `DECDWL` (double-width line) and `DECDHL` (double-height line) escape sequences. Both reset to the default rendition with the `DECSWL` (single-width line) escape sequence. For now this functionality is only supported by the GDI renderer in conhost. There are a lot of changes, so this is just a general overview of the main areas affected. Previously it was safe to assume that the screen had a fixed width, at least for a given point in time. But now we need to deal with the possibility of different lines have different widths, so all the functions that are constrained by the right border (text wrapping, cursor movement operations, and sequences like `EL` and `ICH`) now need to lookup the width of the active line in order to behave correctly. Similarly it used to be safe to assume that buffer and screen coordinates were the same thing, but that is no longer true. Lots of places now need to translate back and forth between coordinate systems dependent on the line rendition. This includes clipboard handling, the conhost color selection and search, accessibility location tracking and screen reading, IME editor positioning, "snapping" the viewport, and of course all the rendering calculations. For the rendering itself, I've had to introduce a new `PrepareLineTransform` method that the render engines can use to setup the necessary transform matrix for a given line rendition. This is also now used to handle the horizontal viewport offset, since that could no longer be achieved just by changing the target coordinates (on a double width line, the viewport offset may be halfway through a character). I've also had to change the renderer's existing `InvalidateCursor` method to take a `SMALL_RECT` rather than a `COORD`, to allow for the cursor being a variable width. Technically this was already a problem, because the cursor could occupy two screen cells when over a double-width character, but now it can be anything between one and four screen cells (e.g. a double-width character on the double-width line). In terms of architectural changes, there is now a new `lineRendition` field in the `ROW` class that keeps track of the line rendition for each row, and several new methods in the `ROW` and `TextBuffer` classes for manipulating that state. This includes a few helper methods for handling the various issues discussed above, e.g. position clamping and translating between coordinate systems. ## Validation Steps Performed I've manually confirmed all the double-width and double-height tests in _Vttest_ are now working as expected, and the _VT100 Torture Test_ now renders correctly (at least the line rendition aspects). I've also got my own test scripts that check many of the line rendition boundary cases and have confirmed that those are now passing. I've manually tested as many areas of the conhost UI that I could think of, that might be affected by line rendition, including things like searching, selection, copying, and color highlighting. For accessibility, I've confirmed that the _Magnifier_ and _Narrator_ correctly handle double-width lines. And I've also tested the Japanese IME, which while not perfect, is at least useable. Closes #7865
2021-02-18 06:44:50 +01:00
// Convert the cursor buffer position to the equivalent screen
// coordinates, taking line rendition into account.
pos = screenInfo.GetTextBuffer().BufferToScreenPosition(pos);
const auto view = screenInfo.GetViewport();
// Set cursor position relative to viewport
// Set up our iterators. We will walk through the entire set of cells from beginning to end.
// The first time, we will give the iterators as the whole span and the begin
// will be moved forward by the conversion area write to set up the next call.
auto begin = cells.cbegin();
const auto end = cells.cend();
// Write over and over updating the beginning iterator until we reach the end.
do
{
begin = _WriteConversionArea(begin, end, pos, view, screenInfo);
} while (begin < end);
}
// Routine Description:
// - Takes the final text string and injects it into the input buffer
// Arguments:
// - text - The text to inject into the input buffer
void ConsoleImeInfo::_InsertConvertedString(const std::wstring_view text)
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& screenInfo = gci.GetActiveOutputBuffer();
if (screenInfo.GetTextBuffer().GetCursor().IsOn())
{
gci.GetCursorBlinker().TimerRoutine(screenInfo);
}
const DWORD dwControlKeyState = GetControlKeyState(0);
std::deque<std::unique_ptr<IInputEvent>> inEvents;
KeyEvent keyEvent{ TRUE, // keydown
1, // repeatCount
0, // virtualKeyCode
0, // virtualScanCode
0, // charData
dwControlKeyState }; // activeModifierKeys
for (const auto& ch : text)
{
keyEvent.SetCharData(ch);
inEvents.push_back(std::make_unique<KeyEvent>(keyEvent));
}
gci.pInputBuffer->Write(inEvents);
}
// Routine Description:
// - Backs up the global cursor visibility state if it is shown and disables
// it while we work on the conversion areas.
Make sure cursor visibility is restored after using an IME (#6207) ## Summary of the Pull Request When using an _Input Method Editor_ in conhost for East Asian languages, the text cursor is temporarily hidden while the characters are being composed. When the composition is complete, the cursor visibility is meant to be restored, but that doesn't always happen if the IME composition is cancelled. This PR makes sure the cursor visibility is always restored, regardless of how the IME is closed. ## PR Checklist * [x] Closes #810 * [x] CLA signed. * [ ] Tests added/passed * [ ] Requires documentation to be updated * [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. ## Detailed Description of the Pull Request / Additional comments The original implementation hid the cursor whenever `ConsoleImeInfo::WriteCompMessage` was called (which could be multiple times in the course of a composition), and then only restored the visibility when `ConsoleImeInfo::WriteResultMessage` was called. If a composition is cancelled, though, `WriteResultMessage` would never be called, so the cursor visibility wouldn't be restored. I've now made the `SaveCursorVisibility` and `RestoreCursorVisibility` methods public, so they can instead be called from the `ImeStartComposition` and `ImeEndComposition` functions. This makes sure `RestoreCursorVisibility` is always called, regardless of how the composition ended, and `SaveCursorVisibility` is only called once at the start of the composition (which isn't essential, but seems cleaner to me). ## Validation Steps Performed I've manually tested opening and closing the IME, both while submitting characters and while cancelling a composition, and in all cases the cursor visibility was correctly restored.
2020-05-27 18:31:09 +02:00
void ConsoleImeInfo::SaveCursorVisibility()
{
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
Cursor& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor();
// Cursor turn OFF.
if (cursor.IsVisible())
{
_isSavedCursorVisible = true;
cursor.SetIsVisible(false);
}
}
// Routine Description:
// - Restores the global cursor visibility state if it was on when it was backed up.
Make sure cursor visibility is restored after using an IME (#6207) ## Summary of the Pull Request When using an _Input Method Editor_ in conhost for East Asian languages, the text cursor is temporarily hidden while the characters are being composed. When the composition is complete, the cursor visibility is meant to be restored, but that doesn't always happen if the IME composition is cancelled. This PR makes sure the cursor visibility is always restored, regardless of how the IME is closed. ## PR Checklist * [x] Closes #810 * [x] CLA signed. * [ ] Tests added/passed * [ ] Requires documentation to be updated * [ ] I've discussed this with core contributors already. If not checked, I'm ready to accept this work might be rejected in favor of a different grand plan. ## Detailed Description of the Pull Request / Additional comments The original implementation hid the cursor whenever `ConsoleImeInfo::WriteCompMessage` was called (which could be multiple times in the course of a composition), and then only restored the visibility when `ConsoleImeInfo::WriteResultMessage` was called. If a composition is cancelled, though, `WriteResultMessage` would never be called, so the cursor visibility wouldn't be restored. I've now made the `SaveCursorVisibility` and `RestoreCursorVisibility` methods public, so they can instead be called from the `ImeStartComposition` and `ImeEndComposition` functions. This makes sure `RestoreCursorVisibility` is always called, regardless of how the composition ended, and `SaveCursorVisibility` is only called once at the start of the composition (which isn't essential, but seems cleaner to me). ## Validation Steps Performed I've manually tested opening and closing the IME, both while submitting characters and while cancelling a composition, and in all cases the cursor visibility was correctly restored.
2020-05-27 18:31:09 +02:00
void ConsoleImeInfo::RestoreCursorVisibility()
{
if (_isSavedCursorVisible)
{
_isSavedCursorVisible = false;
CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
Cursor& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor();
cursor.SetIsVisible(true);
}
}