diff --git a/src/buffer/out/Row.cpp b/src/buffer/out/Row.cpp index 77451dd1cca..62e4269bf5a 100644 --- a/src/buffer/out/Row.cpp +++ b/src/buffer/out/Row.cpp @@ -1143,6 +1143,13 @@ til::CoordType ROW::GetTrailingColumnAtCharOffset(const ptrdiff_t offset) const return _createCharToColumnMapper(offset).GetTrailingColumnAt(offset); } +uint16_t ROW::GetCharOffset(til::CoordType col) const noexcept +{ + const auto columns = GetReadableColumnCount(); + const auto colBeg = clamp(col, 0, columns); + return _uncheckedCharOffset(gsl::narrow_cast(colBeg)); +} + DelimiterClass ROW::DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept { const auto col = _clampedColumn(column); diff --git a/src/buffer/out/Row.hpp b/src/buffer/out/Row.hpp index a1efe36707b..d1157e06e3f 100644 --- a/src/buffer/out/Row.hpp +++ b/src/buffer/out/Row.hpp @@ -172,6 +172,7 @@ class ROW final std::wstring_view GetText(til::CoordType columnBegin, til::CoordType columnEnd) const noexcept; til::CoordType GetLeadingColumnAtCharOffset(ptrdiff_t offset) const noexcept; til::CoordType GetTrailingColumnAtCharOffset(ptrdiff_t offset) const noexcept; + uint16_t GetCharOffset(til::CoordType col) const noexcept; DelimiterClass DelimiterClassAt(til::CoordType column, const std::wstring_view& wordDelimiters) const noexcept; auto AttrBegin() const noexcept { return _attr.begin(); } diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index c32a67dd086..4a98b757a39 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -1061,38 +1061,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) ROW* rowBackup = nullptr; if (row == compositionRow) { - auto& scratch = buffer.GetScratchpadRow(); - scratch.CopyFrom(r); - rowBackup = &scratch; - - std::wstring_view text{ activeComposition.text }; - RowWriteState state{ - .columnLimit = r.GetReadableColumnCount(), - .columnEnd = _compositionCache->absoluteOrigin.x, - }; - - size_t off = 0; - for (const auto& range : activeComposition.attributes) - { - const auto len = range.len; - auto attr = range.attr; - - // Use the color at the cursor if TSF didn't specify any explicit color. - if (attr.GetBackground().IsDefault()) - { - attr.SetBackground(_compositionCache->baseAttribute.GetBackground()); - } - if (attr.GetForeground().IsDefault()) - { - attr.SetForeground(_compositionCache->baseAttribute.GetForeground()); - } - - state.text = text.substr(off, len); - state.columnBegin = state.columnEnd; - const_cast(r).ReplaceText(state); - const_cast(r).ReplaceAttributes(state.columnBegin, state.columnEnd, attr); - off += len; - } + rowBackup = _PaintBufferOutputComposition(buffer, r, activeComposition); } const auto restore = wil::scope_exit([&] { if (rowBackup) @@ -1132,6 +1101,107 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) } } +ROW* Renderer::_PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, const Composition& activeComposition) +{ + auto& scratch = buffer.GetScratchpadRow(); + scratch.CopyFrom(r); + + // *Overwrite* the original text with the active composition... + til::CoordType compositionEnd = 0; + { + std::wstring_view text{ activeComposition.text }; + RowWriteState state{ + .columnLimit = r.GetReadableColumnCount(), + .columnEnd = _compositionCache->absoluteOrigin.x, + }; + + size_t off = 0; + for (const auto& range : activeComposition.attributes) + { + const auto len = range.len; + auto attr = range.attr; + + // Use the color at the cursor if TSF didn't specify any explicit color. + if (attr.GetBackground().IsDefault()) + { + attr.SetBackground(_compositionCache->baseAttribute.GetBackground()); + } + if (attr.GetForeground().IsDefault()) + { + attr.SetForeground(_compositionCache->baseAttribute.GetForeground()); + } + + state.text = text.substr(off, len); + state.columnBegin = state.columnEnd; + const_cast(r).ReplaceText(state); + const_cast(r).ReplaceAttributes(state.columnBegin, state.columnEnd, attr); + off += len; + } + + compositionEnd = state.columnEnd; + } + + // The text we've overwritten may have been crucial to the user, + // so copy it back by absorbing available whitespace to the right + // and re-inserting the non-whitespace characters instead. + const auto compositionWidth = compositionEnd - _compositionCache->absoluteOrigin.x; + const auto colLimit = r.GetReadableColumnCount(); + if (compositionWidth > 0 && compositionEnd < colLimit) + { + const auto text = scratch.GetText(); + auto srcCol = _compositionCache->absoluteOrigin.x; + auto dstCol = compositionEnd; + auto remaining = compositionWidth; + size_t i = scratch.GetCharOffset(srcCol); + + while (i < text.size() && dstCol < colLimit) + { + // Treat whitespace we encounter as a credit towards our composition width. + // This loop essentially absorbs the whitespace. + while (i < text.size() && til::at(text, i) == L' ' && remaining > 0) + { + remaining--; + srcCol++; + i++; + } + if (remaining <= 0) + { + break; + } + + // Find the end of the non-whitespace span: Our span of text to insert. + auto spanEnd = i; + while (spanEnd < text.size() && til::at(text, spanEnd) != L' ') + { + spanEnd++; + } + + // Copy the non-whitespace segment from the original text (scratch) back in. + RowCopyTextFromState state{ + .source = scratch, + .columnBegin = dstCol, + .columnLimit = colLimit, + .sourceColumnBegin = srcCol, + .sourceColumnLimit = scratch.GetLeadingColumnAtCharOffset(spanEnd), + }; + const_cast(r).CopyTextFrom(state); + + const auto srcBeg = gsl::narrow_cast(srcCol); + const auto srcEnd = gsl::narrow_cast(state.sourceColumnEnd); + const auto attr = scratch.Attributes().slice(srcBeg, srcEnd); + const auto dstBeg = gsl::narrow_cast(dstCol); + const auto dstEnd = gsl::narrow_cast(dstCol + attr.size()); + const_cast(r).Attributes().replace(dstBeg, dstEnd, attr); + + dstCol = state.columnEnd; + srcCol = state.sourceColumnEnd; + i = spanEnd; + } + } + + return &scratch; +} + static bool _IsAllSpaces(const std::wstring_view v) { // first non-space char is not found (is npos) diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 399171f0e63..31fc7cc24df 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -121,6 +121,7 @@ namespace Microsoft::Console::Render void _scheduleRenditionBlink(); [[nodiscard]] HRESULT _PaintBackground(_In_ IRenderEngine* const pEngine); void _PaintBufferOutput(_In_ IRenderEngine* const pEngine); + ROW* _PaintBufferOutputComposition(TextBuffer& buffer, const ROW& r, const Composition& activeComposition); void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, TextBufferCellIterator it, const til::point target); void _PaintBufferOutputGridLineHelper(_In_ IRenderEngine* const pEngine, const TextAttribute textAttribute, const size_t cchLine, const til::point coordTarget); bool _isHoveredHyperlink(const TextAttribute& textAttribute) const noexcept;