// 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 attributes, const gsl::span 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: // - // 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(); const TextAttribute fill = gci.GetActiveOutputBuffer().GetAttributes(); 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 attributes, const gsl::span 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 ConsoleImeInfo::s_ConvertToCells(const std::wstring_view text, const gsl::span attributes, const gsl::span colorArray) { std::vector 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::const_iterator ConsoleImeInfo::_WriteConversionArea(const std::vector::const_iterator begin, const std::vector::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 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(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(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 attributes, const gsl::span 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(); 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> 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)); } 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. 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. void ConsoleImeInfo::RestoreCursorVisibility() { if (_isSavedCursorVisible) { _isSavedCursorVisible = false; CONSOLE_INFORMATION& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); Cursor& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); cursor.SetIsVisible(true); } }