-
Notifications
You must be signed in to change notification settings - Fork 8.3k
/
conimeinfo.cpp
515 lines (432 loc) · 20.5 KB
/
conimeinfo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
// 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 til::size 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 auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto bufferSize = gci.GetActiveOutputBuffer().GetBufferSize().Dimensions();
bufferSize.Y = 1;
const auto windowSize = gci.GetActiveOutputBuffer().GetViewport().Dimensions();
const auto fill = gci.GetActiveOutputBuffer().GetAttributes();
const auto popupFill = gci.GetActiveOutputBuffer().GetPopupAttributes();
const auto& 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.
auto 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++)
{
auto 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,
til::point& 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.
// 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--;
}
// GH#12730 - if the lineVec would now be empty, just return early. Failing
// to do so will later cause a crash trying to construct an empty view.
if (lineEnd <= lineBegin)
{
return 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 til::inclusive_rect region{ insertionPos.X, 0, gsl::narrow<til::CoordType>(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.
if (screenInfo.HasAccessibilityEventing())
{
screenInfo.NotifyAccessibilityEventing(region.left, insertionPos.Y, region.right, 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)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& 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();
// 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)
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& screenInfo = gci.GetActiveOutputBuffer();
if (screenInfo.GetTextBuffer().GetCursor().IsOn())
{
gci.GetCursorBlinker().TimerRoutine(screenInfo);
}
const auto 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.
void ConsoleImeInfo::SaveCursorVisibility()
{
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& 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;
auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation();
auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor();
cursor.SetIsVisible(true);
}
}