Skip to content

Commit 7a83c0f

Browse files
drvosslhecker
andauthored
Fix Korean IME overlay hiding characters to the right of cursor (#20041)
This commit fixes a regression in v1.24, where composing Korean text in-between existing characters causes text to the right to be hidden. This is problematic for the Korean IME, because it commonly inserts the composed syllable between existing characters. The fix compresses (removes/absorbs) available whitespace to the right of the composition to make space for any existing text. This means that a composition now virtually acts in insert mode. Closes #20040 Refs #19738 Refs #20039 ## Validation Steps Performed 1. Korean IME: compose `ㄷ` between `가` and `나` → display shows `가ㄷ나`, `나` visible. 2. TUI box (vim prompt with padding): compose inside padded text box → border preserved in place. 3. Composition at end of line (no chars to right): unaffected. 4. English and other IME input: not affected (no active composition row). Co-authored-by: jason <drvoss@users.noreply.github.com> Co-authored-by: Leonard Hecker <lhecker@microsoft.com>
1 parent b2666fb commit 7a83c0f

File tree

4 files changed

+111
-32
lines changed

4 files changed

+111
-32
lines changed

src/buffer/out/Row.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,13 @@ til::CoordType ROW::GetTrailingColumnAtCharOffset(const ptrdiff_t offset) const
11431143
return _createCharToColumnMapper(offset).GetTrailingColumnAt(offset);
11441144
}
11451145

1146+
uint16_t ROW::GetCharOffset(til::CoordType col) const noexcept
1147+
{
1148+
const auto columns = GetReadableColumnCount();
1149+
const auto colBeg = clamp(col, 0, columns);
1150+
return _uncheckedCharOffset(gsl::narrow_cast<size_t>(colBeg));
1151+
}
1152+
11461153
DelimiterClass ROW::DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept
11471154
{
11481155
const auto col = _clampedColumn(column);

src/buffer/out/Row.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ class ROW final
172172
std::wstring_view GetText(til::CoordType columnBegin, til::CoordType columnEnd) const noexcept;
173173
til::CoordType GetLeadingColumnAtCharOffset(ptrdiff_t offset) const noexcept;
174174
til::CoordType GetTrailingColumnAtCharOffset(ptrdiff_t offset) const noexcept;
175+
uint16_t GetCharOffset(til::CoordType col) const noexcept;
175176
DelimiterClass DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept;
176177

177178
auto AttrBegin() const noexcept { return _attr.begin(); }

src/renderer/base/renderer.cpp

Lines changed: 102 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,38 +1061,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
10611061
ROW* rowBackup = nullptr;
10621062
if (row == compositionRow)
10631063
{
1064-
auto& scratch = buffer.GetScratchpadRow();
1065-
scratch.CopyFrom(r);
1066-
rowBackup = &scratch;
1067-
1068-
std::wstring_view text{ activeComposition.text };
1069-
RowWriteState state{
1070-
.columnLimit = r.GetReadableColumnCount(),
1071-
.columnEnd = _compositionCache->absoluteOrigin.x,
1072-
};
1073-
1074-
size_t off = 0;
1075-
for (const auto& range : activeComposition.attributes)
1076-
{
1077-
const auto len = range.len;
1078-
auto attr = range.attr;
1079-
1080-
// Use the color at the cursor if TSF didn't specify any explicit color.
1081-
if (attr.GetBackground().IsDefault())
1082-
{
1083-
attr.SetBackground(_compositionCache->baseAttribute.GetBackground());
1084-
}
1085-
if (attr.GetForeground().IsDefault())
1086-
{
1087-
attr.SetForeground(_compositionCache->baseAttribute.GetForeground());
1088-
}
1089-
1090-
state.text = text.substr(off, len);
1091-
state.columnBegin = state.columnEnd;
1092-
const_cast<ROW&>(r).ReplaceText(state);
1093-
const_cast<ROW&>(r).ReplaceAttributes(state.columnBegin, state.columnEnd, attr);
1094-
off += len;
1095-
}
1064+
rowBackup = _PaintBufferOutputComposition(buffer, r, activeComposition);
10961065
}
10971066
const auto restore = wil::scope_exit([&] {
10981067
if (rowBackup)
@@ -1132,6 +1101,107 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine)
11321101
}
11331102
}
11341103

1104+
ROW* Renderer::_PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, const Composition& activeComposition)
1105+
{
1106+
auto& scratch = buffer.GetScratchpadRow();
1107+
scratch.CopyFrom(r);
1108+
1109+
// *Overwrite* the original text with the active composition...
1110+
til::CoordType compositionEnd = 0;
1111+
{
1112+
std::wstring_view text{ activeComposition.text };
1113+
RowWriteState state{
1114+
.columnLimit = r.GetReadableColumnCount(),
1115+
.columnEnd = _compositionCache->absoluteOrigin.x,
1116+
};
1117+
1118+
size_t off = 0;
1119+
for (const auto& range : activeComposition.attributes)
1120+
{
1121+
const auto len = range.len;
1122+
auto attr = range.attr;
1123+
1124+
// Use the color at the cursor if TSF didn't specify any explicit color.
1125+
if (attr.GetBackground().IsDefault())
1126+
{
1127+
attr.SetBackground(_compositionCache->baseAttribute.GetBackground());
1128+
}
1129+
if (attr.GetForeground().IsDefault())
1130+
{
1131+
attr.SetForeground(_compositionCache->baseAttribute.GetForeground());
1132+
}
1133+
1134+
state.text = text.substr(off, len);
1135+
state.columnBegin = state.columnEnd;
1136+
const_cast<ROW&>(r).ReplaceText(state);
1137+
const_cast<ROW&>(r).ReplaceAttributes(state.columnBegin, state.columnEnd, attr);
1138+
off += len;
1139+
}
1140+
1141+
compositionEnd = state.columnEnd;
1142+
}
1143+
1144+
// The text we've overwritten may have been crucial to the user,
1145+
// so copy it back by absorbing available whitespace to the right
1146+
// and re-inserting the non-whitespace characters instead.
1147+
const auto compositionWidth = compositionEnd - _compositionCache->absoluteOrigin.x;
1148+
const auto colLimit = r.GetReadableColumnCount();
1149+
if (compositionWidth > 0 && compositionEnd < colLimit)
1150+
{
1151+
const auto text = scratch.GetText();
1152+
auto srcCol = _compositionCache->absoluteOrigin.x;
1153+
auto dstCol = compositionEnd;
1154+
auto remaining = compositionWidth;
1155+
size_t i = scratch.GetCharOffset(srcCol);
1156+
1157+
while (i < text.size() && dstCol < colLimit)
1158+
{
1159+
// Treat whitespace we encounter as a credit towards our composition width.
1160+
// This loop essentially absorbs the whitespace.
1161+
while (i < text.size() && til::at(text, i) == L' ' && remaining > 0)
1162+
{
1163+
remaining--;
1164+
srcCol++;
1165+
i++;
1166+
}
1167+
if (remaining <= 0)
1168+
{
1169+
break;
1170+
}
1171+
1172+
// Find the end of the non-whitespace span: Our span of text to insert.
1173+
auto spanEnd = i;
1174+
while (spanEnd < text.size() && til::at(text, spanEnd) != L' ')
1175+
{
1176+
spanEnd++;
1177+
}
1178+
1179+
// Copy the non-whitespace segment from the original text (scratch) back in.
1180+
RowCopyTextFromState state{
1181+
.source = scratch,
1182+
.columnBegin = dstCol,
1183+
.columnLimit = colLimit,
1184+
.sourceColumnBegin = srcCol,
1185+
.sourceColumnLimit = scratch.GetLeadingColumnAtCharOffset(spanEnd),
1186+
};
1187+
const_cast<ROW&>(r).CopyTextFrom(state);
1188+
1189+
const auto srcBeg = gsl::narrow_cast<uint16_t>(srcCol);
1190+
const auto srcEnd = gsl::narrow_cast<uint16_t>(state.sourceColumnEnd);
1191+
const auto attr = scratch.Attributes().slice(srcBeg, srcEnd);
1192+
const auto dstBeg = gsl::narrow_cast<uint16_t>(dstCol);
1193+
const auto dstEnd = gsl::narrow_cast<uint16_t>(dstCol + attr.size());
1194+
const_cast<ROW&>(r).Attributes().replace(dstBeg, dstEnd, attr);
1195+
1196+
dstCol = state.columnEnd;
1197+
srcCol = state.sourceColumnEnd;
1198+
i = spanEnd;
1199+
}
1200+
}
1201+
1202+
return &scratch;
1203+
}
1204+
11351205
static bool _IsAllSpaces(const std::wstring_view v)
11361206
{
11371207
// first non-space char is not found (is npos)

src/renderer/base/renderer.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ namespace Microsoft::Console::Render
121121
void _scheduleRenditionBlink();
122122
[[nodiscard]] HRESULT _PaintBackground(_In_ IRenderEngine* const pEngine);
123123
void _PaintBufferOutput(_In_ IRenderEngine* const pEngine);
124+
ROW* _PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, const Composition& activeComposition);
124125
void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, TextBufferCellIterator it, const til::point target);
125126
void _PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, const TextAttribute textAttribute, const size_t cchLine, const til::point coordTarget);
126127
bool _isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept;

0 commit comments

Comments
 (0)