From b31059e53e23f4d7dabe466cf4c46c08cafb6fba Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 6 May 2024 20:20:40 +0200 Subject: [PATCH 01/39] AtlasEngine: Fix several error handling bugs (#17193) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes: * `HRESULT`s not being shown as unsigned hex * `D2DERR_RECREATE_TARGET` not being handled * 4 calls not checking their `HRESULT` return Out of the 4 only `CreateCompatibleRenderTarget` will throw in practice, however it throws `D2DERR_RECREATE_TARGET` which is common. Without this error handling, AtlasEngine may crash. ## Validation Steps Performed * Set Graphics API to Direct2D * Use `DXGIAdapterRemovalSupportTest.exe` to trigger `D2DERR_RECREATE_TARGET` * No error message is shown ✅ * If the `D2DERR_RECREATE_TARGET` handling is removed, the application never crashes due to `cursorRenderTarget` being `nullptr` ✅ --- src/cascadia/TerminalControl/TermControl.cpp | 4 +++- src/renderer/atlas/AtlasEngine.r.cpp | 2 +- src/renderer/atlas/BackendD2D.cpp | 11 ++++++----- src/renderer/atlas/BackendD2D.h | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index e68810d192f..df1e15164aa 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1120,7 +1120,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation co_return; } - const auto hr = args.Result(); + // HRESULT is a signed 32-bit integer which would result in a hex output like "-0x7766FFF4", + // but canonically HRESULTs are displayed unsigned as "0x8899000C". See GH#11556. + const auto hr = std::bit_cast(args.Result()); const auto parameter = args.Parameter(); winrt::hstring message; diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index f0a448d491b..a0dbdcc5470 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -55,7 +55,7 @@ catch (const wil::ResultException& exception) { const auto hr = exception.GetErrorCode(); - if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) + if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET || hr == D2DERR_RECREATE_TARGET) { _p.dxgi = {}; return E_PENDING; diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index 4310bed3f89..d0909de361f 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -148,11 +148,11 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) _viewportCellCount = p.s->viewportCellCount; } -void BackendD2D::_drawBackground(const RenderingPayload& p) noexcept +void BackendD2D::_drawBackground(const RenderingPayload& p) { if (_backgroundBitmapGeneration != p.colorBitmapGenerations[0]) { - _backgroundBitmap->CopyFromMemory(nullptr, p.backgroundBitmap.data(), gsl::narrow_cast(p.colorBitmapRowStride * sizeof(u32))); + THROW_IF_FAILED(_backgroundBitmap->CopyFromMemory(nullptr, p.backgroundBitmap.data(), gsl::narrow_cast(p.colorBitmapRowStride * sizeof(u32)))); _backgroundBitmapGeneration = p.colorBitmapGenerations[0]; } @@ -356,7 +356,7 @@ f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 _glyphMetrics = Buffer{ size }; } - glyphRun.fontFace->GetDesignGlyphMetrics(glyphRun.glyphIndices, glyphRun.glyphCount, _glyphMetrics.data(), false); + THROW_IF_FAILED(glyphRun.fontFace->GetDesignGlyphMetrics(glyphRun.glyphIndices, glyphRun.glyphCount, _glyphMetrics.data(), false)); const f32 fontScale = glyphRun.fontEmSize / fontMetrics.designUnitsPerEm; f32r accumulatedBounds{ @@ -541,7 +541,8 @@ void BackendD2D::_resizeCursorBitmap(const RenderingPayload& p, const til::size const D2D1_SIZE_F sizeF{ static_cast(newSizeInPx.width), static_cast(newSizeInPx.height) }; const D2D1_SIZE_U sizeU{ gsl::narrow_cast(newSizeInPx.width), gsl::narrow_cast(newSizeInPx.height) }; wil::com_ptr cursorRenderTarget; - _renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, nullptr, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, cursorRenderTarget.addressof()); + THROW_IF_FAILED(_renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, nullptr, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, cursorRenderTarget.addressof())); + cursorRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); cursorRenderTarget->BeginDraw(); @@ -553,7 +554,7 @@ void BackendD2D::_resizeCursorBitmap(const RenderingPayload& p, const til::size } THROW_IF_FAILED(cursorRenderTarget->EndDraw()); - cursorRenderTarget->GetBitmap(_cursorBitmap.put()); + THROW_IF_FAILED(cursorRenderTarget->GetBitmap(_cursorBitmap.put())); _cursorBitmapSize = newSize; } diff --git a/src/renderer/atlas/BackendD2D.h b/src/renderer/atlas/BackendD2D.h index 3bc40bb22cd..e6993d60603 100644 --- a/src/renderer/atlas/BackendD2D.h +++ b/src/renderer/atlas/BackendD2D.h @@ -17,7 +17,7 @@ namespace Microsoft::Console::Render::Atlas private: ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p); - void _drawBackground(const RenderingPayload& p) noexcept; + void _drawBackground(const RenderingPayload& p); void _drawText(RenderingPayload& p); ATLAS_ATTR_COLD f32 _drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept; ATLAS_ATTR_COLD void _drawTextResetLineRendition(const ShapedRow* row) const noexcept; From 432dfcc4902d1a8393217a5f529d07e65182a3f8 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Mon, 6 May 2024 20:18:24 +0100 Subject: [PATCH 02/39] Prevent the VT engine painting unnecessarily (#17194) When the VT render engine starts a paint operation, it first checks to see whether there is actually something to do, and if not it can end the frame early. However, the result of that check was being ignored, which could sometimes result in an unwanted `SGR` reset being written to the conpty pipe. This was particular concerning when passing through `DCS` sequences, because an unexpected `SGR` in the middle of the `DCS` string would cause it to abort early. This PR addresses the problem by making sure the `VtEngine::StartPaint` return value is appropriately handled in the `XtermEngine` class. ## Detailed Description of the Pull Request / Additional comments To make this work, I also needed to correct the `_cursorMoved` flag, because that is one of things that determines whether a paint is needed or not, but it was being set in the `InvalidateCursor` method at the start of ever frame, regardless of whether the cursor had actually moved. I also took this opportunity to get rid of the `_WillWriteSingleChar` method and the `_quickReturn` flag, which have been mostly obsolete for a long time now. The only place the flag was still used was to optimize single char writes when line renditions are active. But that could more easily be handled by testing the `_invalidMap` directly. ## Validation Steps Performed I've confirmed that the test case in issue #17117 is no longer aborting the `DCS` color table sequence early. Closes #17117 --- src/renderer/vt/XtermEngine.cpp | 20 +++++++++---------- src/renderer/vt/invalidate.cpp | 2 +- src/renderer/vt/math.cpp | 35 --------------------------------- src/renderer/vt/paint.cpp | 16 +++++++-------- src/renderer/vt/state.cpp | 1 - src/renderer/vt/vtrenderer.hpp | 3 --- 6 files changed, 19 insertions(+), 58 deletions(-) diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index 75fc3510fa6..b6d0c58a862 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -37,7 +37,16 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // the pipe. [[nodiscard]] HRESULT XtermEngine::StartPaint() noexcept { - RETURN_IF_FAILED(VtEngine::StartPaint()); + const auto hr = VtEngine::StartPaint(); + if (hr != S_OK) + { + // If _noFlushOnEnd was set, and we didn't return early, it would usually + // have been reset in the EndPaint call. But since that's not going to + // happen now, we need to reset it here, otherwise we may mistakenly skip + // the flush on the next frame. + _noFlushOnEnd = false; + return hr; + } _trace.TraceLastText(_lastText); @@ -83,15 +92,6 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, } } - if (!_quickReturn) - { - if (_WillWriteSingleChar()) - { - // Don't re-enable the cursor. - _quickReturn = true; - } - } - return S_OK; } diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp index 489a6b50bc1..942ba58df26 100644 --- a/src/renderer/vt/invalidate.cpp +++ b/src/renderer/vt/invalidate.cpp @@ -75,7 +75,7 @@ CATCH_RETURN(); } _skipCursor = false; - _cursorMoved = true; + _cursorMoved = psrRegion->origin() != _lastText; return S_OK; } diff --git a/src/renderer/vt/math.cpp b/src/renderer/vt/math.cpp index d86e4644ca5..354ca0e9925 100644 --- a/src/renderer/vt/math.cpp +++ b/src/renderer/vt/math.cpp @@ -52,38 +52,3 @@ void VtEngine::_OrRect(_Inout_ til::inclusive_rect* const pRectExisting, const t pRectExisting->right = std::max(pRectExisting->right, pRectToOr->right); pRectExisting->bottom = std::max(pRectExisting->bottom, pRectToOr->bottom); } - -// Method Description: -// - Returns true if the invalidated region indicates that we only need to -// simply print text from the current cursor position. This will prevent us -// from sending extra VT set-up/tear down sequences (?12h/l) when all we -// need to do is print more text at the current cursor position. -// Arguments: -// - -// Return Value: -// - true iff only the next character is invalid -bool VtEngine::_WillWriteSingleChar() const -{ - // If there is no scroll delta, return false. - if (til::point{ 0, 0 } != _scrollDelta) - { - return false; - } - - // If there is more than one invalid char, return false. - if (!_invalidMap.one()) - { - return false; - } - - // Get the single point at which things are invalid. - const auto invalidPoint = _invalidMap.runs().front().origin(); - - // Either the next character to the right or the immediately previous - // character should follow this code path - // (The immediate previous character would suggest a backspace) - auto invalidIsNext = invalidPoint == _lastText; - auto invalidIsLast = invalidPoint == til::point{ _lastText.x - 1, _lastText.y }; - - return invalidIsNext || invalidIsLast; -} diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index af08cdd5b58..1fcb5335887 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -20,30 +20,30 @@ using namespace Microsoft::Console::Types; // HRESULT error code if painting didn't start successfully. [[nodiscard]] HRESULT VtEngine::StartPaint() noexcept { + // When unit testing, there may be no pipe, but we still need to paint. if (!_hFile) { - return S_FALSE; + return S_OK; } // If we're using line renditions, and this is a full screen paint, we can // potentially stop using them at the end of this frame. _stopUsingLineRenditions = _usingLineRenditions && _AllIsInvalid(); - // If there's nothing to do, quick return + // If there's nothing to do, we won't need to paint. auto somethingToDo = _invalidMap.any() || _scrollDelta != til::point{ 0, 0 } || _cursorMoved || _titleChanged; - _quickReturn = !somethingToDo; - _trace.TraceStartPaint(_quickReturn, + _trace.TraceStartPaint(!somethingToDo, _invalidMap, _lastViewport.ToExclusive(), _scrollDelta, _cursorMoved, _wrappedRow); - return _quickReturn ? S_FALSE : S_OK; + return somethingToDo ? S_OK : S_FALSE; } // Routine Description: @@ -142,9 +142,9 @@ using namespace Microsoft::Console::Types; _usingLineRenditions = true; } // One simple optimization is that we can skip sending the line attributes - // when _quickReturn is true. That indicates that we're writing out a single - // character, which should preclude there being a rendition switch. - if (_usingLineRenditions && !_quickReturn) + // when we're writing out a single character, which should preclude there + // being a rendition switch. + if (_usingLineRenditions && !_invalidMap.one()) { RETURN_IF_FAILED(_MoveCursor({ _lastText.x, targetRow })); switch (lineRendition) diff --git a/src/renderer/vt/state.cpp b/src/renderer/vt/state.cpp index 881fb0c2cce..f192536e3e6 100644 --- a/src/renderer/vt/state.cpp +++ b/src/renderer/vt/state.cpp @@ -37,7 +37,6 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe, _pool(til::pmr::get_default_resource()), _invalidMap(initialViewport.Dimensions(), false, &_pool), _scrollDelta(0, 0), - _quickReturn(false), _clearedAllThisFrame(false), _cursorMoved(false), _resized(false), diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 0125fe17369..b5c487fc1c0 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -113,7 +113,6 @@ namespace Microsoft::Console::Render til::point _lastText; til::point _scrollDelta; - bool _quickReturn; bool _clearedAllThisFrame; bool _cursorMoved; bool _resized; @@ -214,8 +213,6 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT _RgbUpdateDrawingBrushes(const TextAttribute& textAttributes) noexcept; [[nodiscard]] HRESULT _16ColorUpdateDrawingBrushes(const TextAttribute& textAttributes) noexcept; - bool _WillWriteSingleChar() const; - // buffer space for these two functions to build their lines // so they don't have to alloc/free in a tight loop std::wstring _bufferLine; From 80d2e5894413ab04ea0ac0b87273751f4160f52e Mon Sep 17 00:00:00 2001 From: Windows Console Service Bot <14666831+consvc@users.noreply.github.com> Date: Mon, 6 May 2024 14:35:11 -0500 Subject: [PATCH 03/39] Localization Updates - 05/03/2024 19:01:37 (#17188) --- .../TerminalSettingsEditor/Resources/es-ES/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/it-IT/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/ja-JP/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/ko-KR/Resources.resw | 6 ++++++ .../TerminalSettingsEditor/Resources/pt-BR/Resources.resw | 6 ++++++ 5 files changed, 30 insertions(+) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw index 06ef4c0b3c2..61ef4151625 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw @@ -733,10 +733,16 @@ Examinar... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Agregar nuevo eje de fuente + Agregar nuevo Button label that adds a new font axis for the current font. + + Agregar nueva característica de fuente + Agregar nuevo Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw index 6cbfe190b22..1b2550d1d61 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw @@ -733,10 +733,16 @@ Sfoglia... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Aggiungi nuovo asse dei tipi di carattere + Aggiungi nuovo Button label that adds a new font axis for the current font. + + Aggiungi nuova funzionalità dei tipi di carattere + Aggiungi nuovo Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw index 70e4da642b0..3ba41206daa 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw @@ -733,10 +733,16 @@ 参照... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + 新しいフォント軸の追加 + 新規追加 Button label that adds a new font axis for the current font. + + 新しいフォント機能の追加 + 新規追加 Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw index 04bf3054043..4e022bd43b8 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw @@ -733,10 +733,16 @@ 찾아보기... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + 새 글꼴 추가 축 + 새로 추가 Button label that adds a new font axis for the current font. + + 새 글꼴 추가 기능 + 새로 추가 Button label that adds a new font feature for the current font. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw index 09611a80f06..8ac46dede93 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw @@ -733,10 +733,16 @@ Procurar... Button label that opens a file picker in a new window. The "..." is standard to mean it will open a new window. + + Adicionar novo eixo de fonte + Adicionar novo Button label that adds a new font axis for the current font. + + Adicionar novo recurso de fonte + Adicionar novo Button label that adds a new font feature for the current font. From 9c16c5ca82943c2c466fb16a093a559d53a2fb0c Mon Sep 17 00:00:00 2001 From: James Holderness Date: Mon, 6 May 2024 21:03:33 +0100 Subject: [PATCH 04/39] Make sure DCS strings are flushed to conpty without delay (#17195) When the `DCS` passthrough code was first implemented, it relied on the `ActionPassThroughString` method flushing the given string immediately. However, that has since stopped being the case, so `DCS` operations end up being delayed until the entire sequence has been parsed. This PR fixes the issue by introducing a `flush` parameter to force an immediate flush on the `ActionPassThroughString` method, as well as the `XtermEngine::WriteTerminalW` method that it calls. ## Validation Steps Performed I've confirmed that the test case in issue #17111 now updates the color table as soon as each color entry is parsed, instead of delaying the updates until the end of the sequence. Closes #17111 --- src/renderer/vt/XtermEngine.cpp | 10 +++++++--- src/renderer/vt/XtermEngine.hpp | 2 +- src/renderer/vt/vtrenderer.hpp | 2 +- src/terminal/adapter/adaptDispatch.cpp | 2 +- src/terminal/parser/IStateMachineEngine.hpp | 2 +- src/terminal/parser/InputStateMachineEngine.cpp | 3 ++- src/terminal/parser/InputStateMachineEngine.hpp | 2 +- src/terminal/parser/OutputStateMachineEngine.cpp | 5 +++-- src/terminal/parser/OutputStateMachineEngine.hpp | 2 +- src/terminal/parser/ut_parser/StateMachineTest.cpp | 2 +- 10 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index b6d0c58a862..9c489aa29cc 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -520,9 +520,10 @@ CATCH_RETURN(); // proper utf-8 string, depending on our mode. // Arguments: // - wstr - wstring of text to be written +// - flush - set to true if the string should be flushed immediately // Return Value: // - S_OK or suitable HRESULT error from either conversion or writing pipe. -[[nodiscard]] HRESULT XtermEngine::WriteTerminalW(const std::wstring_view wstr) noexcept +[[nodiscard]] HRESULT XtermEngine::WriteTerminalW(const std::wstring_view wstr, const bool flush) noexcept { RETURN_IF_FAILED(_fUseAsciiOnly ? VtEngine::_WriteTerminalAscii(wstr) : @@ -535,8 +536,11 @@ CATCH_RETURN(); // cause flickering (where we've buffered some state but not the whole // "frame" as specified by the app). We'll just immediately buffer this // sequence, and flush it when the render thread comes around to paint the - // frame normally. - + // frame normally, unless a flush has been explicitly requested. + if (flush) + { + _flushImpl(); + } return S_OK; } diff --git a/src/renderer/vt/XtermEngine.hpp b/src/renderer/vt/XtermEngine.hpp index 6f1b480b61e..7449fb3e38b 100644 --- a/src/renderer/vt/XtermEngine.hpp +++ b/src/renderer/vt/XtermEngine.hpp @@ -51,7 +51,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT InvalidateScroll(const til::point* const pcoordDelta) noexcept override; - [[nodiscard]] HRESULT WriteTerminalW(const std::wstring_view str) noexcept override; + [[nodiscard]] HRESULT WriteTerminalW(const std::wstring_view str, const bool flush) noexcept override; [[nodiscard]] HRESULT SetWindowVisibility(const bool showOrHide) noexcept override; diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index b5c487fc1c0..26fc67cb138 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -78,7 +78,7 @@ namespace Microsoft::Console::Render [[nodiscard]] HRESULT RequestCursor() noexcept; [[nodiscard]] HRESULT InheritCursor(const til::point coordCursor) noexcept; [[nodiscard]] HRESULT WriteTerminalUtf8(const std::string_view str) noexcept; - [[nodiscard]] virtual HRESULT WriteTerminalW(const std::wstring_view str) noexcept = 0; + [[nodiscard]] virtual HRESULT WriteTerminalW(const std::wstring_view str, const bool flush = false) noexcept = 0; void SetTerminalOwner(Microsoft::Console::VirtualTerminal::VtIo* const terminalOwner); void SetResizeQuirk(const bool resizeQuirk); void SetLookingForDSRCallback(std::function pfnLooking) noexcept; diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index bf038dd75ee..02c22db8eec 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -4882,7 +4882,7 @@ ITermDispatch::StringHandler AdaptDispatch::_CreatePassthroughHandler() { buffer += L'\\'; } - engine.ActionPassThroughString(buffer); + engine.ActionPassThroughString(buffer, true); buffer.clear(); } return !endOfString; diff --git a/src/terminal/parser/IStateMachineEngine.hpp b/src/terminal/parser/IStateMachineEngine.hpp index 1eefc5be2a5..d1d838ccc5e 100644 --- a/src/terminal/parser/IStateMachineEngine.hpp +++ b/src/terminal/parser/IStateMachineEngine.hpp @@ -35,7 +35,7 @@ namespace Microsoft::Console::VirtualTerminal virtual bool ActionPrint(const wchar_t wch) = 0; virtual bool ActionPrintString(const std::wstring_view string) = 0; - virtual bool ActionPassThroughString(const std::wstring_view string) = 0; + virtual bool ActionPassThroughString(const std::wstring_view string, const bool flush = false) = 0; virtual bool ActionEscDispatch(const VTID id) = 0; virtual bool ActionVt52EscDispatch(const VTID id, const VTParameters parameters) = 0; diff --git a/src/terminal/parser/InputStateMachineEngine.cpp b/src/terminal/parser/InputStateMachineEngine.cpp index df7a8c2cfb7..4a25d056f49 100644 --- a/src/terminal/parser/InputStateMachineEngine.cpp +++ b/src/terminal/parser/InputStateMachineEngine.cpp @@ -276,9 +276,10 @@ bool InputStateMachineEngine::ActionPrintString(const std::wstring_view string) // string of characters given. // Arguments: // - string - string to dispatch. +// - flush - not applicable to the input state machine. // Return Value: // - true iff we successfully dispatched the sequence. -bool InputStateMachineEngine::ActionPassThroughString(const std::wstring_view string) +bool InputStateMachineEngine::ActionPassThroughString(const std::wstring_view string, const bool /*flush*/) { if (_pDispatch->IsVtInputEnabled()) { diff --git a/src/terminal/parser/InputStateMachineEngine.hpp b/src/terminal/parser/InputStateMachineEngine.hpp index 6b90c729492..4b17669aedc 100644 --- a/src/terminal/parser/InputStateMachineEngine.hpp +++ b/src/terminal/parser/InputStateMachineEngine.hpp @@ -142,7 +142,7 @@ namespace Microsoft::Console::VirtualTerminal bool ActionPrintString(const std::wstring_view string) override; - bool ActionPassThroughString(const std::wstring_view string) override; + bool ActionPassThroughString(const std::wstring_view string, const bool flush) override; bool ActionEscDispatch(const VTID id) override; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index 34d40fc850d..f4b3fcbf677 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -180,14 +180,15 @@ bool OutputStateMachineEngine::ActionPrintString(const std::wstring_view string) // we don't know what to do with it) // Arguments: // - string - string to dispatch. +// - flush - set to true if the string should be flushed immediately. // Return Value: // - true iff we successfully dispatched the sequence. -bool OutputStateMachineEngine::ActionPassThroughString(const std::wstring_view string) +bool OutputStateMachineEngine::ActionPassThroughString(const std::wstring_view string, const bool flush) { auto success = true; if (_pTtyConnection != nullptr) { - const auto hr = _pTtyConnection->WriteTerminalW(string); + const auto hr = _pTtyConnection->WriteTerminalW(string, flush); LOG_IF_FAILED(hr); success = SUCCEEDED(hr); } diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 1ff22ab5a99..64fbf81503e 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -38,7 +38,7 @@ namespace Microsoft::Console::VirtualTerminal bool ActionPrintString(const std::wstring_view string) override; - bool ActionPassThroughString(const std::wstring_view string) override; + bool ActionPassThroughString(const std::wstring_view string, const bool flush) override; bool ActionEscDispatch(const VTID id) override; diff --git a/src/terminal/parser/ut_parser/StateMachineTest.cpp b/src/terminal/parser/ut_parser/StateMachineTest.cpp index 0bf36050e95..f910a72c1a4 100644 --- a/src/terminal/parser/ut_parser/StateMachineTest.cpp +++ b/src/terminal/parser/ut_parser/StateMachineTest.cpp @@ -59,7 +59,7 @@ class Microsoft::Console::VirtualTerminal::TestStateMachineEngine : public IStat return true; }; - bool ActionPassThroughString(const std::wstring_view string) override + bool ActionPassThroughString(const std::wstring_view string, const bool /*flush*/) override { passedThrough += string; return true; From 6d0342f0bb31bf245843411c6781d6d5399ff651 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 7 May 2024 20:35:48 +0200 Subject: [PATCH 05/39] Add nullptr checks to shared_ptr conversions (#17199) We use `if (auto self = weakSelf.get())` in a lot of places. That assigns the value to `self` and then checks if it's truthy. Sometimes we need to add a "is (app) closing" check because XAML, so we wrote something akin to `if (self = ...; !closing)`. But that's wrong because the correct `if (foo)` is the same as `if (void; foo)` and not `if (foo; void)` and that meant that we didn't check for `self`'s truthiness anymore. This issue became apparent now, because we added a new kind of delayed callback invocation (which is a lot cheaper). This made the lack of a `nullptr` check finally obvious. --- src/cascadia/TerminalControl/ControlCore.cpp | 4 ++-- src/cascadia/TerminalControl/TermControl.cpp | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index da78e6167a1..575864ede1a 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -161,7 +161,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::chrono::milliseconds{ 100 }, [weakTerminal = std::weak_ptr{ _terminal }, weakThis = get_weak(), dispatcher = _dispatcher]() { dispatcher.TryEnqueue(DispatcherQueuePriority::Normal, [weakThis]() { - if (const auto self = weakThis.get(); !self->_IsClosing()) + if (const auto self = weakThis.get(); self && !self->_IsClosing()) { self->OutputIdle.raise(*self, nullptr); } @@ -179,7 +179,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _dispatcher, std::chrono::milliseconds{ 8 }, [weakThis = get_weak()](const auto& update) { - if (auto core{ weakThis.get() }; !core->_IsClosing()) + if (auto core{ weakThis.get() }; core && !core->_IsClosing()) { core->ScrollPositionChanged.raise(*core, update); } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index df1e15164aa..c1c7cc73e53 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -248,7 +248,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation dispatcher, TerminalWarningBellInterval, [weakThis = get_weak()]() { - if (auto control{ weakThis.get() }; !control->_IsClosing()) + if (auto control{ weakThis.get() }; control && !control->_IsClosing()) { control->WarningBell.raise(*control, nullptr); } @@ -258,7 +258,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation dispatcher, ScrollBarUpdateInterval, [weakThis = get_weak()](const auto& update) { - if (auto control{ weakThis.get() }; !control->_IsClosing()) + if (auto control{ weakThis.get() }; control && !control->_IsClosing()) { control->_throttledUpdateScrollbar(update); } @@ -301,7 +301,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _originalSelectedSecondaryElements.Append(e); } ContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) { - if (auto control{ weakThis.get() }; !control->_IsClosing()) + if (auto control{ weakThis.get() }; control && !control->_IsClosing()) { const auto& menu{ control->ContextMenu() }; menu.PrimaryCommands().Clear(); @@ -317,7 +317,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } }); SelectionContextMenu().Closed([weakThis = get_weak()](auto&&, auto&&) { - if (auto control{ weakThis.get() }; !control->_IsClosing()) + if (auto control{ weakThis.get() }; control && !control->_IsClosing()) { const auto& menu{ control->SelectionContextMenu() }; menu.PrimaryCommands().Clear(); @@ -544,7 +544,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } _searchBox->Open([weakThis = get_weak()]() { - if (const auto self = weakThis.get(); !self->_IsClosing()) + if (const auto self = weakThis.get(); self && !self->_IsClosing()) { self->_searchBox->SetFocusOnTextbox(); self->_refreshSearch(); From 0b76c51ba10c81ba4213ba98cf88f2abdf91b15e Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 8 May 2024 22:24:21 +0200 Subject: [PATCH 06/39] Fix !_noFlushOnEnd not flushing (#17212) This simply copies a bit more from `VtEngine::EndPaint`'s `_noFlushOnEnd` handling which already seems to fix the linked issue. Closes #17204 --- src/renderer/vt/XtermEngine.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index 9c489aa29cc..4df161befc1 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -44,6 +44,10 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, // have been reset in the EndPaint call. But since that's not going to // happen now, we need to reset it here, otherwise we may mistakenly skip // the flush on the next frame. + if (!_noFlushOnEnd) + { + _Flush(); + } _noFlushOnEnd = false; return hr; } From dbac3a1fa3755737be0d761e6e44e86b58e2865e Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 8 May 2024 22:52:52 +0200 Subject: [PATCH 07/39] Fix session being persisted even when disabled (#17211) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes 2 bugs: * `PersistState` being called when the window is closed (as opposed to closing the tab). The settings check was missing. * Session cleanup running depending on whether the feature is currently enabled as opposed to whether it was enabled on launch. Closes #17206 Closes #17207 ## Validation Steps Performed * Create a bunch of leftover buffer_*.txt files by running the current Dev version off of main * Build this branch, then open and close a window * All buffer_*.txt are gone and state.json is cleaned up ✅ --- src/cascadia/TerminalApp/TerminalWindow.cpp | 2 +- src/cascadia/TerminalApp/TerminalWindow.h | 2 +- src/cascadia/TerminalApp/TerminalWindow.idl | 2 +- src/cascadia/WindowsTerminal/AppHost.cpp | 9 ++++++--- src/cascadia/WindowsTerminal/WindowEmperor.cpp | 10 +++++++++- src/cascadia/WindowsTerminal/WindowEmperor.h | 1 + 6 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp index 749fd2e842c..c04a34affc9 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.cpp +++ b/src/cascadia/TerminalApp/TerminalWindow.cpp @@ -263,7 +263,7 @@ namespace winrt::TerminalApp::implementation AppLogic::Current()->NotifyRootInitialized(); } - void TerminalWindow::Quit() + void TerminalWindow::PersistState() { if (_root) { diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h index b10c29a8d72..40e94a39201 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.h +++ b/src/cascadia/TerminalApp/TerminalWindow.h @@ -73,7 +73,7 @@ namespace winrt::TerminalApp::implementation void Create(); - void Quit(); + void PersistState(); winrt::fire_and_forget UpdateSettings(winrt::TerminalApp::SettingsLoadEventArgs args); diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl index 75c47fce8bf..2cf4b317298 100644 --- a/src/cascadia/TerminalApp/TerminalWindow.idl +++ b/src/cascadia/TerminalApp/TerminalWindow.idl @@ -61,7 +61,7 @@ namespace TerminalApp Boolean ShouldImmediatelyHandoffToElevated(); void HandoffToElevated(); - void Quit(); + void PersistState(); Windows.UI.Xaml.UIElement GetRoot(); diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp index 94585f4391c..63d8de231d3 100644 --- a/src/cascadia/WindowsTerminal/AppHost.cpp +++ b/src/cascadia/WindowsTerminal/AppHost.cpp @@ -1184,13 +1184,16 @@ winrt::fire_and_forget AppHost::_QuitRequested(const winrt::Windows::Foundation: co_await wil::resume_foreground(_windowLogic.GetRoot().Dispatcher()); const auto strongThis = weakThis.lock(); - // GH #16235: If we don't have a window logic, we're already refrigerating, and won't have our _window either. - if (!strongThis || _windowLogic == nullptr) + if (!strongThis) { co_return; } - _windowLogic.Quit(); + if (_appLogic && _windowLogic && _appLogic.ShouldUsePersistedLayout()) + { + _windowLogic.PersistState(); + } + PostQuitMessage(0); } diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp index a8f0961e420..1e78e721fd4 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp +++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp @@ -348,6 +348,14 @@ void WindowEmperor::_becomeMonarch() _revokers.WindowCreated = _manager.WindowCreated(winrt::auto_revoke, { this, &WindowEmperor::_numberOfWindowsChanged }); _revokers.WindowClosed = _manager.WindowClosed(winrt::auto_revoke, { this, &WindowEmperor::_numberOfWindowsChanged }); + + // If a previous session of Windows Terminal stored buffer_*.txt files, then we need to clean all those up on exit + // that aren't needed anymore, even if the user disabled the ShouldUsePersistedLayout() setting in the meantime. + { + const auto state = ApplicationState::SharedInstance(); + const auto layouts = state.PersistedWindowLayouts(); + _requiresPersistenceCleanupOnExit = layouts && layouts.Size() > 0; + } } // sender and args are always nullptr @@ -486,7 +494,7 @@ void WindowEmperor::_finalizeSessionPersistence() const // Ensure to write the state.json before we TerminateProcess() state.Flush(); - if (!_app.Logic().ShouldUsePersistedLayout()) + if (!_requiresPersistenceCleanupOnExit) { return; } diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.h b/src/cascadia/WindowsTerminal/WindowEmperor.h index a35032e6d5e..d4186cbbb06 100644 --- a/src/cascadia/WindowsTerminal/WindowEmperor.h +++ b/src/cascadia/WindowsTerminal/WindowEmperor.h @@ -51,6 +51,7 @@ class WindowEmperor : public std::enable_shared_from_this std::unique_ptr _notificationIcon; + bool _requiresPersistenceCleanupOnExit = false; bool _quitting{ false }; void _windowStartedHandlerPostXAML(const std::shared_ptr& sender); From 49e4eea60f737b46b8aeda505f4693df8a9d44a6 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 8 May 2024 22:58:19 +0200 Subject: [PATCH 08/39] Fix typing multiple emojis on Windows 10 (#17213) On Windows 10 Emojis don't finish composition until the Emoji picker panel is closed. Each emoji is thus its own composition range. `firstRange` thus caused only the first emoji to finish composition. The end result was that all remaining emojis would stay around forever, with the user entirely unable to clear them. ## Validation Steps Performed * Windows 10 VM * Open Emoji picker (Win+.) * Press and hold Enter on any Emoji * Press Esc to finish the composition * All of the Emoji can be backspaced / deleted --- src/tsf/Implementation.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tsf/Implementation.cpp b/src/tsf/Implementation.cpp index d3ad65ae9c5..aa7ace81fca 100644 --- a/src/tsf/Implementation.cpp +++ b/src/tsf/Implementation.cpp @@ -436,7 +436,7 @@ void Implementation::_doCompositionUpdate(TfEditCookie ec) std::wstring finalizedString; std::wstring activeComposition; til::small_vector activeCompositionRanges; - bool firstRange = true; + bool activeCompositionEncountered = false; const GUID* guids[] = { &GUID_PROP_COMPOSING, &GUID_PROP_ATTRIBUTE }; wil::com_ptr props; @@ -500,7 +500,9 @@ void Implementation::_doCompositionUpdate(TfEditCookie ec) ULONG len = bufCap; THROW_IF_FAILED(range->GetText(ec, TF_TF_MOVESTART, buf, len, &len)); - if (!composing && firstRange) + // Since we can't un-finalize finalized text, we only finalize text that's at the start of the document. + // In other words, don't put text that's in the middle between two active compositions into the finalized string. + if (!composing && !activeCompositionEncountered) { finalizedString.append(buf, len); } @@ -520,7 +522,7 @@ void Implementation::_doCompositionUpdate(TfEditCookie ec) const auto attr = _textAttributeFromAtom(atom); activeCompositionRanges.emplace_back(totalLen, attr); - firstRange = false; + activeCompositionEncountered |= composing; } } From 44516ad7cffef1996cabc5bf7435937ee32ba51d Mon Sep 17 00:00:00 2001 From: krzysdz Date: Thu, 9 May 2024 18:29:47 +0000 Subject: [PATCH 09/39] PowerShell menu completion parser thread-safety fix (#17221) Fix Terminal crashing when experimental PowerShell menu completion is very quickly invoked multiple times. `Command::ParsePowerShellMenuComplete` can be called from multiple threads, but it uses a `static` `Json::CharReader`, which cannot safely parse data from multiple threads at the same time. Removing `static` fixes the problem, since every function call gets its own `reader`. Validation: Pressed Ctrl+Space quickly a few times with hardcoded huge JSON as the completion payload. Also shown at the end of the second video in #17220. Closes #17220 --- src/cascadia/TerminalSettingsModel/Command.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cascadia/TerminalSettingsModel/Command.cpp b/src/cascadia/TerminalSettingsModel/Command.cpp index a32ed4efa79..70f63091a1f 100644 --- a/src/cascadia/TerminalSettingsModel/Command.cpp +++ b/src/cascadia/TerminalSettingsModel/Command.cpp @@ -690,7 +690,7 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation auto data = winrt::to_string(json); std::string errs; - static std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; + std::unique_ptr reader{ Json::CharReaderBuilder{}.newCharReader() }; Json::Value root; if (!reader->parse(data.data(), data.data() + data.size(), &root, &errs)) { From 34ecc5bf239fa175d2e4fe55d1e77be40cf55866 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Fri, 10 May 2024 01:42:38 +0100 Subject: [PATCH 10/39] Fix conpty cursor movement detection on double-width lines (#17233) When the VT render engine checks whether the cursor has moved in the `InvalidateCursor` method, it does so by comparing the origin of the given cursor region with the last text output coordinates. But these two values are actually from different coordinate systems, and when on a double-width line, the x text coordinate is half of the corresponding screen coordinate. As a result, the movement detection is sometimes incorrect. This PR fixes the issue by adding another field to track the last cursor origin in screen coordinates, so we have a meaningful value to compare against. ## References and Relevant Issues The previous cursor movement detection was added in PR #17194 to fix issue #17117. ## Validation Steps Performed I've confirmed that the test case from issue #17232 is now fixed, and the test case from issue #17117 is still working as expected. ## PR Checklist - [x] Closes #17232 --- src/renderer/vt/invalidate.cpp | 3 ++- src/renderer/vt/vtrenderer.hpp | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/vt/invalidate.cpp b/src/renderer/vt/invalidate.cpp index 942ba58df26..d67ca839437 100644 --- a/src/renderer/vt/invalidate.cpp +++ b/src/renderer/vt/invalidate.cpp @@ -75,7 +75,8 @@ CATCH_RETURN(); } _skipCursor = false; - _cursorMoved = psrRegion->origin() != _lastText; + _cursorMoved = psrRegion->origin() != _lastCursorOrigin; + _lastCursorOrigin = psrRegion->origin(); return S_OK; } diff --git a/src/renderer/vt/vtrenderer.hpp b/src/renderer/vt/vtrenderer.hpp index 26fc67cb138..2641614b32d 100644 --- a/src/renderer/vt/vtrenderer.hpp +++ b/src/renderer/vt/vtrenderer.hpp @@ -111,6 +111,7 @@ namespace Microsoft::Console::Render til::pmr::bitmap _invalidMap; til::point _lastText; + til::point _lastCursorOrigin; til::point _scrollDelta; bool _clearedAllThisFrame; From b6f5cbe1ee6574cd365fe027eb703df460c578d1 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Fri, 10 May 2024 02:18:46 +0100 Subject: [PATCH 11/39] Fix cursor invalidation when line renditions are used (#17234) ## Summary of the Pull Request When the renderer calculates the invalidate region for the cursor, it needs to take the line rendition into account. But it was using a relative coordinate rather than absolute coordinate when looking up the line rendition for the row, so the calculated region could easily be incorrect. With this PR we now use the line rendition that was already being cached in the `CursorOptions` structure, so we avoid needing to look it up anyway. Similarly I've replaced the `IsCursorDoubleWidth` lookup with the value that was already cached in the `CursorOptions` structure. ## Validation Steps Performed I've confirmed that the test case in issue #17226 is now working as expected. ## PR Checklist - [x] Closes #17226 --- src/renderer/base/renderer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 91e5772006e..c8ed8b28e7b 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -1178,8 +1178,8 @@ void Renderer::_invalidateCurrentCursor() const const auto view = buffer.GetSize(); const auto coord = _currentCursorOptions.coordCursor; - const auto lineRendition = buffer.GetLineRendition(coord.y); - const auto cursorWidth = _pData->IsCursorDoubleWidth() ? 2 : 1; + const auto lineRendition = _currentCursorOptions.lineRendition; + const auto cursorWidth = _currentCursorOptions.fIsDoubleWidth ? 2 : 1; til::rect rect{ coord.x, coord.y, coord.x + cursorWidth, coord.y + 1 }; rect = BufferToScreenLine(rect, lineRendition); From 5ce7fb74036726c33eb5e565be2ae759aacd72a8 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Fri, 10 May 2024 15:54:56 -0500 Subject: [PATCH 12/39] build: move ESRP to a parameterized subtask which takes signingId (#17216) This centralized all our ESRP calls in one file, which will make it easier in the future when we are invariable required to change how we call it again. --- .../templates-v2/job-build-package-wpf.yml | 56 +++++++------- .../templates-v2/job-build-project.yml | 20 ++--- .../job-merge-msix-into-bundle.yml | 74 +++++++++---------- .../templates-v2/job-package-conpty.yml | 56 +++++++------- .../templates-v2/steps-esrp-signing.yml | 22 ++++++ 5 files changed, 117 insertions(+), 111 deletions(-) create mode 100644 build/pipelines/templates-v2/steps-esrp-signing.yml diff --git a/build/pipelines/templates-v2/job-build-package-wpf.yml b/build/pipelines/templates-v2/job-build-package-wpf.yml index a9656a65e68..dd547502e71 100644 --- a/build/pipelines/templates-v2/job-build-package-wpf.yml +++ b/build/pipelines/templates-v2/job-build-package-wpf.yml @@ -100,36 +100,32 @@ jobs: flattenFolders: true - ${{ if eq(parameters.codeSign, true) }}: - - task: EsrpCodeSigning@5 - displayName: Submit *.nupkg to ESRP for code signing - inputs: - ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} - AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} - AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} - AuthAKVName: ${{ parameters.signingIdentity.akvName }} - AuthCertName: ${{ parameters.signingIdentity.authCertName }} - AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} - FolderPath: $(Build.ArtifactStagingDirectory)/nupkg - Pattern: '*.nupkg' - UseMinimatch: true - signConfigType: inlineSignParams - inlineOperation: >- - [ - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetSign", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetVerify", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] + - template: steps-esrp-signing.yml + parameters: + displayName: Submit *.nupkg to ESRP for code signing + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: $(Build.ArtifactStagingDirectory)/nupkg + Pattern: '*.nupkg' + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] - ${{ if eq(parameters.generateSbom, true) }}: - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 diff --git a/build/pipelines/templates-v2/job-build-project.yml b/build/pipelines/templates-v2/job-build-project.yml index ed3cd0844c2..ae5d3a16ac6 100644 --- a/build/pipelines/templates-v2/job-build-project.yml +++ b/build/pipelines/templates-v2/job-build-project.yml @@ -242,18 +242,14 @@ jobs: # Code-sign everything we just put together. # We run the signing in Terminal.BinDir, because all of the signing batches are relative to the final architecture/configuration output folder. - - task: EsrpCodeSigning@5 - displayName: Submit Signing Request - inputs: - ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} - AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} - AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} - AuthAKVName: ${{ parameters.signingIdentity.akvName }} - AuthCertName: ${{ parameters.signingIdentity.authCertName }} - AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} - FolderPath: '$(Terminal.BinDir)' - signType: batchSigning - batchSignPolicyFile: '$(Build.SourcesDirectory)/ESRPSigningConfig.json' + - template: steps-esrp-signing.yml + parameters: + displayName: Submit Signing Request + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: '$(Terminal.BinDir)' + signType: batchSigning + batchSignPolicyFile: '$(Build.SourcesDirectory)/ESRPSigningConfig.json' # We only need to re-pack the MSIX if we actually signed, so this can stay in the codeSign conditional - ${{ if or(parameters.buildTerminal, parameters.buildEverything) }}: diff --git a/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml index e3644e35bc7..f400e2cb121 100644 --- a/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml +++ b/build/pipelines/templates-v2/job-merge-msix-into-bundle.yml @@ -97,45 +97,41 @@ jobs: displayName: Create msixbundle - ${{ if eq(parameters.codeSign, true) }}: - - task: EsrpCodeSigning@5 - displayName: Submit *.msixbundle to ESRP for code signing - inputs: - ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} - AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} - AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} - AuthAKVName: ${{ parameters.signingIdentity.akvName }} - AuthCertName: ${{ parameters.signingIdentity.authCertName }} - AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} - FolderPath: $(System.ArtifactsDirectory)\bundle - Pattern: $(BundleStemName)*.msixbundle - UseMinimatch: true - signConfigType: inlineSignParams - inlineOperation: >- - [ - { - "KeyCode": "Dynamic", - "CertTemplateName": "WINMSAPP1ST", - "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", - "OperationCode": "SigntoolSign", - "Parameters": { - "OpusName": "Microsoft", - "OpusInfo": "http://www.microsoft.com", - "FileDigest": "/fd \"SHA256\"", - "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" - }, - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "Dynamic", - "CertTemplateName": "WINMSAPP1ST", - "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", - "OperationCode": "SigntoolVerify", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] + - template: steps-esrp-signing.yml + parameters: + displayName: Submit *.msixbundle to ESRP for code signing + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: $(System.ArtifactsDirectory)\bundle + Pattern: $(BundleStemName)*.msixbundle + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "Dynamic", + "CertTemplateName": "WINMSAPP1ST", + "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + "OperationCode": "SigntoolSign", + "Parameters": { + "OpusName": "Microsoft", + "OpusInfo": "http://www.microsoft.com", + "FileDigest": "/fd \"SHA256\"", + "TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256" + }, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "Dynamic", + "CertTemplateName": "WINMSAPP1ST", + "CertSubjectName": "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", + "OperationCode": "SigntoolVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] - ${{ if eq(parameters.generateSbom, true) }}: - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 diff --git a/build/pipelines/templates-v2/job-package-conpty.yml b/build/pipelines/templates-v2/job-package-conpty.yml index e09775f8360..303bb998f5a 100644 --- a/build/pipelines/templates-v2/job-package-conpty.yml +++ b/build/pipelines/templates-v2/job-package-conpty.yml @@ -85,36 +85,32 @@ jobs: versionEnvVar: XES_PACKAGEVERSIONNUMBER - ${{ if eq(parameters.codeSign, true) }}: - - task: EsrpCodeSigning@5 - displayName: Submit *.nupkg to ESRP for code signing - inputs: - ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} - AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} - AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} - AuthAKVName: ${{ parameters.signingIdentity.akvName }} - AuthCertName: ${{ parameters.signingIdentity.authCertName }} - AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} - FolderPath: $(Build.ArtifactStagingDirectory)/nupkg - Pattern: '*.nupkg' - UseMinimatch: true - signConfigType: inlineSignParams - inlineOperation: >- - [ - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetSign", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - }, - { - "KeyCode": "CP-401405", - "OperationCode": "NuGetVerify", - "Parameters": {}, - "ToolName": "sign", - "ToolVersion": "1.0" - } - ] + - template: steps-esrp-signing.yml + parameters: + displayName: Submit *.nupkg to ESRP for code signing + signingIdentity: ${{ parameters.signingIdentity }} + inputs: + FolderPath: $(Build.ArtifactStagingDirectory)/nupkg + Pattern: '*.nupkg' + UseMinimatch: true + signConfigType: inlineSignParams + inlineOperation: >- + [ + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetSign", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + }, + { + "KeyCode": "CP-401405", + "OperationCode": "NuGetVerify", + "Parameters": {}, + "ToolName": "sign", + "ToolVersion": "1.0" + } + ] - ${{ if eq(parameters.generateSbom, true) }}: - task: AzureArtifacts.manifest-generator-task.manifest-generator-task.ManifestGeneratorTask@0 diff --git a/build/pipelines/templates-v2/steps-esrp-signing.yml b/build/pipelines/templates-v2/steps-esrp-signing.yml new file mode 100644 index 00000000000..9a8e6dbd9a7 --- /dev/null +++ b/build/pipelines/templates-v2/steps-esrp-signing.yml @@ -0,0 +1,22 @@ +parameters: + - name: displayName + type: string + default: ESRP Code Signing + - name: inputs + type: object + default: {} + - name: signingIdentity + type: object + default: {} + +steps: + - task: EsrpCodeSigning@5 + displayName: ${{ parameters.displayName }} + inputs: + ConnectedServiceName: ${{ parameters.signingIdentity.serviceName }} + AppRegistrationClientId: ${{ parameters.signingIdentity.appId }} + AppRegistrationTenantId: ${{ parameters.signingIdentity.tenantId }} + AuthAKVName: ${{ parameters.signingIdentity.akvName }} + AuthCertName: ${{ parameters.signingIdentity.authCertName }} + AuthSignCertName: ${{ parameters.signingIdentity.signCertName }} + ${{ insert }}: ${{ parameters.inputs }} From 30ef1f461d778429898542a1da5b799361b169bb Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Mon, 13 May 2024 22:11:08 +0530 Subject: [PATCH 13/39] Fix single-tab window tear-off crash (#17251) ## Validation Steps Performed - Opened multi-tab terminal window with Narrator. Narrator can read characters from the tabs. - Started a drag and drop (tear-off) of a tab, and it didn't crash. This was repeated multiple times. --- src/cascadia/TerminalControl/TermControl.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index c1c7cc73e53..97fbdd46dc8 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1055,8 +1055,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation // MSFT 33353327: We're purposefully not using _initializedTerminal to ensure we're fully initialized. // Doing so makes us return nullptr when XAML requests an automation peer. // Instead, we need to give XAML an automation peer, then fix it later. - if (!_IsClosing()) + if (!_IsClosing() && !_detached) { + // It's unexpected that interactivity is null even when we're not closing or in detached state. + THROW_HR_IF_NULL(E_UNEXPECTED, _interactivity); + // create a custom automation peer with this code pattern: // (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers) if (const auto& interactivityAutoPeer{ _interactivity.OnCreateAutomationPeer() }) From 46526bc00cf3336b4557955e3ee6462afd6b3a46 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 13 May 2024 11:42:01 -0500 Subject: [PATCH 14/39] Remove the animations from the suggestions UI (#17247) gotta go fast, and these animations are not fast noted in #15845 --- .../TerminalApp/SuggestionsControl.xaml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalApp/SuggestionsControl.xaml b/src/cascadia/TerminalApp/SuggestionsControl.xaml index 7350b2146ee..b3e81865b3f 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.xaml +++ b/src/cascadia/TerminalApp/SuggestionsControl.xaml @@ -12,6 +12,7 @@ xmlns:model="using:Microsoft.Terminal.Settings.Model" xmlns:mtu="using:Microsoft.Terminal.UI" xmlns:mux="using:Microsoft.UI.Xaml.Controls" + MinWidth="256" AllowFocusOnInteraction="True" AutomationProperties.Name="{x:Bind ControlName, Mode=OneWay}" IsTabStop="True" @@ -99,6 +100,24 @@ GeneralItemTemplate="{StaticResource GeneralItemTemplate}" NestedItemTemplate="{StaticResource NestedItemTemplate}" /> + + @@ -203,7 +222,8 @@ ItemClick="_listItemClicked" ItemsSource="{x:Bind FilteredActions}" SelectionChanged="_listItemSelectionChanged" - SelectionMode="Single" /> + SelectionMode="Single" + Style="{StaticResource NoAnimationsPlease}" /> From e1b102a3540d7cc5aa24c2e3357e778357a679be Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Mon, 13 May 2024 18:56:21 +0200 Subject: [PATCH 15/39] Fix race conditions in UiaTextRangeBase (#17257) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We need to lock the buffer when getting the viewport/cursor position. This caused the UIA overlay to randomly fail to update. ## Validation Steps Performed * Open a cmd tab and hold any key immediately * Repeat until you're somewhat confident it's gone ✅ --- src/types/UiaTextRangeBase.cpp | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/types/UiaTextRangeBase.cpp b/src/types/UiaTextRangeBase.cpp index fc21128e1aa..ce42bf6096f 100644 --- a/src/types/UiaTextRangeBase.cpp +++ b/src/types/UiaTextRangeBase.cpp @@ -22,10 +22,15 @@ try RETURN_HR_IF_NULL(E_INVALIDARG, pProvider); RETURN_HR_IF_NULL(E_INVALIDARG, pData); + pData->LockConsole(); + const auto unlock = wil::scope_exit([&]() noexcept { + pData->UnlockConsole(); + }); + _pProvider = pProvider; _pData = pData; _start = pData->GetViewport().Origin(); - _end = pData->GetViewport().Origin(); + _end = _start; _blockRange = false; _wordDelimiters = wordDelimiters; @@ -41,9 +46,16 @@ HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ Render::IRenderData* pData _In_ std::wstring_view wordDelimiters) noexcept try { + RETURN_HR_IF_NULL(E_INVALIDARG, pProvider); RETURN_HR_IF_NULL(E_INVALIDARG, pData); - RETURN_IF_FAILED(RuntimeClassInitialize(pData, pProvider, wordDelimiters)); + pData->LockConsole(); + const auto unlock = wil::scope_exit([&]() noexcept { + pData->UnlockConsole(); + }); + + _pProvider = pProvider; + _pData = pData; // GH#8730: The cursor position may be in a delayed state, resulting in it being out of bounds. // If that's the case, clamp it to be within bounds. // TODO GH#12440: We should be able to just check some fields off of the Cursor object, @@ -51,6 +63,8 @@ try _start = cursor.GetPosition(); pData->GetTextBuffer().GetSize().Clamp(_start); _end = _start; + _blockRange = false; + _wordDelimiters = wordDelimiters; UiaTracing::TextRange::Constructor(*this); return S_OK; @@ -66,15 +80,15 @@ HRESULT UiaTextRangeBase::RuntimeClassInitialize(_In_ Render::IRenderData* pData _In_ std::wstring_view wordDelimiters) noexcept try { - RETURN_IF_FAILED(RuntimeClassInitialize(pData, pProvider, wordDelimiters)); + RETURN_HR_IF_NULL(E_INVALIDARG, pProvider); + RETURN_HR_IF_NULL(E_INVALIDARG, pData); - // start must be before or equal to end + _pProvider = pProvider; + _pData = pData; _start = std::min(start, end); _end = std::max(start, end); - - // This should be the only way to set if we are a blockRange - // This is used for blockSelection _blockRange = blockRange; + _wordDelimiters = wordDelimiters; UiaTracing::TextRange::Constructor(*this); return S_OK; @@ -148,9 +162,6 @@ til::point UiaTextRangeBase::GetEndpoint(TextPatternRangeEndpoint endpoint) cons // - true if range is degenerate, false otherwise. bool UiaTextRangeBase::SetEndpoint(TextPatternRangeEndpoint endpoint, const til::point val) noexcept { - // GH#6402: Get the actual buffer size here, instead of the one - // constrained by the virtual bottom. - const auto bufferSize = _pData->GetTextBuffer().GetSize(); switch (endpoint) { case TextPatternRangeEndpoint_End: From bf8a647788f2ca9632bb6c9ab508419e79566754 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 13 May 2024 12:36:27 -0500 Subject: [PATCH 16/39] Clean up command history context passed to suggestions UI (#17245) This is fallout from #16937. * Typing a command then backspacing the chars then asking for suggestions would think the current commandline ended with spaces, making filtering very hard. * The currently typed command would _also_ appear in the command history, which isn't useful. I actually did TDD for this and wrote the test first, then confirmed again running through the build script, I wasn't hitting any of the earlier issues. Closes #17241 Closes #17243 --- src/cascadia/TerminalControl/ControlCore.cpp | 33 +++++++++--- .../UnitTests_Control/ControlCoreTests.cpp | 51 +++++++++++++++++++ 2 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 575864ede1a..c4ad4b0799d 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -2235,20 +2235,39 @@ namespace winrt::Microsoft::Terminal::Control::implementation std::vector commands; const auto bufferCommands{ textBuffer.Commands() }; - for (const auto& commandInBuffer : bufferCommands) - { - const auto strEnd = commandInBuffer.find_last_not_of(UNICODE_SPACE); + + auto trimToHstring = [](const auto& s) -> winrt::hstring { + const auto strEnd = s.find_last_not_of(UNICODE_SPACE); if (strEnd != std::string::npos) { - const auto trimmed = commandInBuffer.substr(0, strEnd + 1); + const auto trimmed = s.substr(0, strEnd + 1); + return winrt::hstring{ trimmed }; + } + return winrt::hstring{ L"" }; + }; + + const auto currentCommand = _terminal->CurrentCommand(); + const auto trimmedCurrentCommand = trimToHstring(currentCommand); - commands.push_back(winrt::hstring{ trimmed }); + for (const auto& commandInBuffer : bufferCommands) + { + if (const auto hstr{ trimToHstring(commandInBuffer) }; + (!hstr.empty() && hstr != trimmedCurrentCommand)) + { + commands.push_back(hstr); } } - auto context = winrt::make_self(std::move(commands)); - context->CurrentCommandline(winrt::hstring{ _terminal->CurrentCommand() }); + // If the very last thing in the list of recent commands, is exactly the + // same as the current command, then let's not include it in the + // history. It's literally the thing the user has typed, RIGHT now. + if (!commands.empty() && commands.back() == trimmedCurrentCommand) + { + commands.pop_back(); + } + auto context = winrt::make_self(std::move(commands)); + context->CurrentCommandline(trimmedCurrentCommand); return *context; } diff --git a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp index 63f20f9058e..c9f2dbb2ae6 100644 --- a/src/cascadia/UnitTests_Control/ControlCoreTests.cpp +++ b/src/cascadia/UnitTests_Control/ControlCoreTests.cpp @@ -40,6 +40,7 @@ namespace ControlUnitTests TEST_METHOD(TestSelectCommandSimple); TEST_METHOD(TestSelectOutputSimple); + TEST_METHOD(TestCommandContext); TEST_METHOD(TestSelectOutputScrolling); TEST_METHOD(TestSelectOutputExactWrap); @@ -509,6 +510,56 @@ namespace ControlUnitTests VERIFY_ARE_EQUAL(expectedEnd, end); } } + void ControlCoreTests::TestCommandContext() + { + auto [settings, conn] = _createSettingsAndConnection(); + Log::Comment(L"Create ControlCore object"); + auto core = createCore(*settings, *conn); + VERIFY_IS_NOT_NULL(core); + _standardInit(core); + + Log::Comment(L"Print some text"); + + _writePrompt(conn, L"C:\\Windows"); + conn->WriteInput(L"Foo-bar"); + conn->WriteInput(L"\x1b]133;C\x7"); + + conn->WriteInput(L"\r\n"); + conn->WriteInput(L"This is some text \r\n"); + conn->WriteInput(L"with varying amounts \r\n"); + conn->WriteInput(L"of whitespace \r\n"); + + _writePrompt(conn, L"C:\\Windows"); + + Log::Comment(L"Check the command context"); + + const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope; + { + auto historyContext{ core->CommandHistory() }; + VERIFY_ARE_EQUAL(1u, historyContext.History().Size()); + VERIFY_ARE_EQUAL(L"", historyContext.CurrentCommandline()); + } + + Log::Comment(L"Write 'Bar' to the command..."); + conn->WriteInput(L"Bar"); + { + auto historyContext{ core->CommandHistory() }; + // Bar shouldn't be in the history, it should be the current command + VERIFY_ARE_EQUAL(1u, historyContext.History().Size()); + VERIFY_ARE_EQUAL(L"Bar", historyContext.CurrentCommandline()); + } + + Log::Comment(L"then delete it"); + conn->WriteInput(L"\b \b"); + conn->WriteInput(L"\b \b"); + conn->WriteInput(L"\b \b"); + { + auto historyContext{ core->CommandHistory() }; + VERIFY_ARE_EQUAL(1u, historyContext.History().Size()); + // The current commandline is now empty + VERIFY_ARE_EQUAL(L"", historyContext.CurrentCommandline()); + } + } void ControlCoreTests::TestSelectOutputScrolling() { From 0d39c008cb2c48ae091675a31fa6014ee826c5b1 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 14 May 2024 17:23:27 +0200 Subject: [PATCH 17/39] Improve font related help text (#17259) A few minor changes to better guide people along new features in 1.21. The font face box gets a sub-text that explains how to add multiple fonts and the builtin glyph toggle now explains its dependence to D3D. --- .../TerminalSettingsEditor/Resources/en-US/Resources.resw | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw index f27f774c55c..2ac6770cab4 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/en-US/Resources.resw @@ -895,6 +895,9 @@ Font face Name for a control to select the font for text in the app. + + You can use multiple fonts by separating them with an ASCII comma. + Font size Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - When enabled, the terminal draws custom glyphs for block element and box drawing characters instead of using the font. This feature only works when GPU Acceleration is available. + When enabled, the terminal draws custom glyphs for block element and box drawing characters instead of using the font. This feature is unavailable when using Direct2D as the Graphics API. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. From bf55c444601117dfb09b405752f4ea105e078997 Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Tue, 14 May 2024 14:19:01 -0500 Subject: [PATCH 18/39] ServicingPipeline: Warn when local release-XX branch is not uptodate (#17260) This prevents me from making dumb mistakes, really. --- tools/ReleaseEngineering/ServicingPipeline.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tools/ReleaseEngineering/ServicingPipeline.ps1 b/tools/ReleaseEngineering/ServicingPipeline.ps1 index 2adf3843a0a..b9d2ba4c86f 100644 --- a/tools/ReleaseEngineering/ServicingPipeline.ps1 +++ b/tools/ReleaseEngineering/ServicingPipeline.ps1 @@ -89,6 +89,20 @@ $Cards = Get-GithubProjectCard -ColumnId $ToPickColumn.id & git fetch --all 2>&1 | Out-Null +$Branch = & git rev-parse --abbrev-ref HEAD +$RemoteForCurrentBranch = & git config "branch.$Branch.remote" +$CommitsBehind = [int](& git rev-list --count "$RemoteForCurrentBranch/$Branch" "^$Branch") + +If ($LASTEXITCODE -Ne 0) { + Write-Error "Failed to determine branch divergence" + Exit 1 +} + +If ($CommitsBehind -Gt 0) { + Write-Error "Local branch $Branch is out of date with $RemoteForCurrentBranch; consider git pull" + Exit 1 +} + $Entries = @(& git log $SourceBranch --first-parent --grep "(#\($($Cards.Number -Join "\|")\))" "--pretty=format:%H%x1C%s" | ConvertFrom-CSV -Delimiter "`u{001C}" -Header CommitID,Subject) From 54cfb857dba0de343112a8a7e4f0bee0bcd39ac5 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 14 May 2024 14:20:06 -0500 Subject: [PATCH 19/39] Remove spaces from snippets in the SXNUI (#17261) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Couple different issues: * The suggestions UI can't filter snippets without a name, that have a space in them, because '␣' != ' '. This instead removes the visualized space from the name shown in the SXN UI. * Similarly, we generate an action with leading backspaces to remove the current commandline. Then we visualize those BS's as a part of the generated name. Same thing - can't filter to that. Before in blue: ![image](https://github.com/microsoft/terminal/assets/18356694/b65e102b-3d23-4d66-9fb9-cfcbb32cf963) ![image](https://github.com/microsoft/terminal/assets/18356694/0a0f4a0e-3ba5-4b61-8f80-1b988fbbb319) closes #16577 closes #16578 --- .../TerminalSettingsModel/ActionMap.cpp | 20 +++++++++++++++++-- src/inc/til/string.h | 18 +++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/cascadia/TerminalSettingsModel/ActionMap.cpp b/src/cascadia/TerminalSettingsModel/ActionMap.cpp index 220cd825565..7e480a33866 100644 --- a/src/cascadia/TerminalSettingsModel/ActionMap.cpp +++ b/src/cascadia/TerminalSettingsModel/ActionMap.cpp @@ -6,6 +6,7 @@ #include "ActionMap.h" #include "Command.h" #include "AllShortcutActions.h" +#include #include "ActionMap.g.cpp" @@ -964,17 +965,32 @@ namespace winrt::Microsoft::Terminal::Settings::Model::implementation cmdImpl.copy_from(winrt::get_self(command)); const auto inArgs{ command.ActionAndArgs().Args().try_as() }; - + const auto inputString{ inArgs ? inArgs.Input() : L"" }; auto args = winrt::make_self( winrt::hstring{ fmt::format(FMT_COMPILE(L"{:\x7f^{}}{}"), L"", numBackspaces, - (std::wstring_view)(inArgs ? inArgs.Input() : L"")) }); + inputString) }); Model::ActionAndArgs actionAndArgs{ ShortcutAction::SendInput, *args }; auto copy = cmdImpl->Copy(); copy->ActionAndArgs(actionAndArgs); + if (!copy->HasName()) + { + // Here, we want to manually generate a send input name, but + // without visualizing space and backspace + // + // This is exactly the body of SendInputArgs::GenerateName, but + // with visualize_nonspace_control_codes instead of + // visualize_control_codes, to make filtering in the suggestions + // UI easier. + + const auto escapedInput = til::visualize_nonspace_control_codes(std::wstring{ inputString }); + const auto name = fmt::format(std::wstring_view(RS_(L"SendInputCommandKey")), escapedInput); + copy->Name(winrt::hstring{ name }); + } + return *copy; }; diff --git a/src/inc/til/string.h b/src/inc/til/string.h index fcb6905e282..5070ae472fe 100644 --- a/src/inc/til/string.h +++ b/src/inc/til/string.h @@ -24,6 +24,24 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned" } return str; } + // The same as the above, but it doesn't visualize BS nor SPC. + _TIL_INLINEPREFIX std::wstring visualize_nonspace_control_codes(std::wstring str) noexcept + { + for (auto& ch : str) + { + // NOT backspace! + if (ch < 0x20 && ch != 0x08) + { + ch += 0x2400; + } + // NOT space + else if (ch == 0x7f) + { + ch = 0x2421; // replace del with ␡ + } + } + return str; + } _TIL_INLINEPREFIX std::wstring visualize_control_codes(std::wstring_view str) { From f62d2d5d2c7b4188daf1d46f48faacf5ee2c717d Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 14 May 2024 21:26:16 +0200 Subject: [PATCH 20/39] AtlasEngine: Improve robustness against weird font sizes (#17258) This clamps the font sizes between 1 and 100. Additionally, it fixes a warning that I randomly noticed when reproducing the issue: D2D complained that `EndDraw` must be called before releasing resources. Finally, this fixes a crash when the terminal size is exactly (1,1) cells, which happened because the initial (invalid) size was (1,1) too. This doesn't fully fix all font-size related issues, but that's currently difficult to achieve, as for instance the swap chain size isn't actually based on the window size, nay, it's based on the cell size multiplied by the cell count. So if the cell size is egregiously large then we get a swap chain size that's larger than the display and potentially larger than what the GPU supports which results in errors. Closes #17227 --- src/renderer/atlas/AtlasEngine.api.cpp | 2 +- src/renderer/atlas/BackendD2D.cpp | 26 ++++++++++++++++++-------- src/renderer/atlas/BackendD3D.cpp | 12 ++++++++++++ src/renderer/atlas/BackendD3D.h | 1 + src/renderer/atlas/common.h | 4 ++-- 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/renderer/atlas/AtlasEngine.api.cpp b/src/renderer/atlas/AtlasEngine.api.cpp index 2fbaf7cd52a..15535ad89f0 100644 --- a/src/renderer/atlas/AtlasEngine.api.cpp +++ b/src/renderer/atlas/AtlasEngine.api.cpp @@ -636,7 +636,7 @@ void AtlasEngine::_resolveFontMetrics(const FontInfoDesired& fontInfoDesired, Fo const auto& faceName = fontInfoDesired.GetFaceName(); const auto requestedFamily = fontInfoDesired.GetFamily(); auto requestedWeight = fontInfoDesired.GetWeight(); - auto fontSize = fontInfoDesired.GetFontSize(); + auto fontSize = std::clamp(fontInfoDesired.GetFontSize(), 1.0f, 100.0f); auto requestedSize = fontInfoDesired.GetEngineSize(); if (!requestedSize.height) diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index d0909de361f..5a663b49d04 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -39,18 +39,28 @@ void BackendD2D::Render(RenderingPayload& p) } _renderTarget->BeginDraw(); + try + { #if ATLAS_DEBUG_SHOW_DIRTY || ATLAS_DEBUG_DUMP_RENDER_TARGET - // Invalidating the render target helps with spotting Present1() bugs. - _renderTarget->Clear(); + // Invalidating the render target helps with spotting Present1() bugs. + _renderTarget->Clear(); #endif - _drawBackground(p); - _drawCursorPart1(p); - _drawText(p); - _drawCursorPart2(p); - _drawSelection(p); + _drawBackground(p); + _drawCursorPart1(p); + _drawText(p); + _drawCursorPart2(p); + _drawSelection(p); #if ATLAS_DEBUG_SHOW_DIRTY - _debugShowDirty(p); + _debugShowDirty(p); #endif + } + catch (...) + { + // In case an exception is thrown for some reason between BeginDraw() and EndDraw() + // we still technically need to call EndDraw() before releasing any resources. + LOG_IF_FAILED(_renderTarget->EndDraw()); + throw; + } THROW_IF_FAILED(_renderTarget->EndDraw()); #if ATLAS_DEBUG_DUMP_RENDER_TARGET diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index e5b3d061f54..fe07271c16a 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -184,6 +184,18 @@ BackendD3D::BackendD3D(const RenderingPayload& p) #endif } +#pragma warning(suppress : 26432) // If you define or delete any default operation in the type '...', define or delete them all (c.21). +BackendD3D::~BackendD3D() +{ + // In case an exception is thrown for some reason between BeginDraw() and EndDraw() + // we still technically need to call EndDraw() before releasing any resources. + if (_d2dBeganDrawing) + { +#pragma warning(suppress : 26447) // The function is declared 'noexcept' but calls function '...' which may throw exceptions (f.6). + LOG_IF_FAILED(_d2dRenderTarget->EndDraw()); + } +} + void BackendD3D::ReleaseResources() noexcept { _renderTargetView.reset(); diff --git a/src/renderer/atlas/BackendD3D.h b/src/renderer/atlas/BackendD3D.h index ae9803d51f8..218df1f0179 100644 --- a/src/renderer/atlas/BackendD3D.h +++ b/src/renderer/atlas/BackendD3D.h @@ -13,6 +13,7 @@ namespace Microsoft::Console::Render::Atlas struct BackendD3D : IBackend { BackendD3D(const RenderingPayload& p); + ~BackendD3D() override; void ReleaseResources() noexcept override; void Render(RenderingPayload& payload) override; diff --git a/src/renderer/atlas/common.h b/src/renderer/atlas/common.h index 120b3d86662..53b093e8a49 100644 --- a/src/renderer/atlas/common.h +++ b/src/renderer/atlas/common.h @@ -404,9 +404,9 @@ namespace Microsoft::Console::Render::Atlas til::generational cursor; til::generational misc; // Size of the viewport / swap chain in pixel. - u16x2 targetSize{ 1, 1 }; + u16x2 targetSize{ 0, 0 }; // Size of the portion of the text buffer that we're drawing on the screen. - u16x2 viewportCellCount{ 1, 1 }; + u16x2 viewportCellCount{ 0, 0 }; // The position of the viewport inside the text buffer (in cells). u16x2 viewportOffset{ 0, 0 }; }; From 9054c819349fa8a052f52ed4d03c5656bec4ff9c Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 May 2024 00:03:04 +0200 Subject: [PATCH 21/39] Fix persistence of handoff'd tabs (#17268) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As it turns out, for handoff'd connections `Initialize` isn't called and this meant the `_sessionId` was always null. After this PR we still don't have a `_profileGuid` but that's probably not a critical issue, since that's an inherent flaw with handoff. It can only be solved in a robust manner if WT gets launched before the console app is launched, but it's unlikely for that to ever happen. ## Validation Steps Performed * Launch * Register that version of WT as the default * Close all tabs (Ctrl+Shift+W) * `persistedWindowLayouts` is empty ✅ * Launch cmd/pwsh via handoff * You get 1 window ✅ * Close the window (= press the X button) * Launch * You get 2 windows ✅ --- src/cascadia/TerminalApp/TerminalPage.cpp | 7 +++++-- src/cascadia/TerminalConnection/ConptyConnection.cpp | 4 +++- src/cascadia/TerminalConnection/ConptyConnection.h | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index 3a82a05aebf..ca86bb80a37 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -1899,7 +1899,10 @@ namespace winrt::TerminalApp::implementation void TerminalPage::PersistState() { - if (_startupState != StartupState::Initialized) + // This method may be called for a window even if it hasn't had a tab yet or lost all of them. + // We shouldn't persist such windows. + const auto tabCount = _tabs.Size(); + if (_startupState != StartupState::Initialized || tabCount == 0) { return; } @@ -1915,7 +1918,7 @@ namespace winrt::TerminalApp::implementation // if the focused tab was not the last tab, restore that auto idx = _GetFocusedTabIndex(); - if (idx && idx != _tabs.Size() - 1) + if (idx && idx != tabCount - 1) { ActionAndArgs action; action.Action(ShortcutAction::SwitchToTab); diff --git a/src/cascadia/TerminalConnection/ConptyConnection.cpp b/src/cascadia/TerminalConnection/ConptyConnection.cpp index dd85388263b..7d326ea65c1 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.cpp +++ b/src/cascadia/TerminalConnection/ConptyConnection.cpp @@ -180,12 +180,14 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const HANDLE hRef, const HANDLE hServerProcess, const HANDLE hClientProcess, - TERMINAL_STARTUP_INFO startupInfo) : + const TERMINAL_STARTUP_INFO& startupInfo) : _rows{ 25 }, _cols{ 80 }, _inPipe{ hIn }, _outPipe{ hOut } { + _sessionId = Utils::CreateGuid(); + THROW_IF_FAILED(ConptyPackPseudoConsole(hServerProcess, hRef, hSig, &_hPC)); _piClient.hProcess = hClientProcess; diff --git a/src/cascadia/TerminalConnection/ConptyConnection.h b/src/cascadia/TerminalConnection/ConptyConnection.h index e1b47bee64e..246621c5eeb 100644 --- a/src/cascadia/TerminalConnection/ConptyConnection.h +++ b/src/cascadia/TerminalConnection/ConptyConnection.h @@ -19,7 +19,7 @@ namespace winrt::Microsoft::Terminal::TerminalConnection::implementation const HANDLE hRef, const HANDLE hServerProcess, const HANDLE hClientProcess, - TERMINAL_STARTUP_INFO startupInfo); + const TERMINAL_STARTUP_INFO& startupInfo); ConptyConnection() noexcept = default; void Initialize(const Windows::Foundation::Collections::ValueSet& settings); From 183a8956f699e1967d4cb4712184d0dbc395a0e5 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 May 2024 00:03:28 +0200 Subject: [PATCH 22/39] Fix lock warning during ReturnResponse (#17266) As reported here: https://github.com/microsoft/terminal/pull/16224#discussion_r1594849244 The underlying `WriteFile` call may block indefinitely and we shouldn't hold the terminal lock during that period. --- src/cascadia/TerminalCore/TerminalApi.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 42f1c903c33..9b85b18d5ff 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -22,8 +22,9 @@ TRACELOGGING_DEFINE_PROVIDER(g_hCTerminalCoreProvider, void Terminal::ReturnResponse(const std::wstring_view response) { - if (_pfnWriteInput) + if (_pfnWriteInput && !response.empty()) { + const auto suspension = _readWriteLock.suspend(); _pfnWriteInput(response); } } From 3486111722296f287158e0340789c607642c1067 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Fri, 17 May 2024 01:41:48 +0200 Subject: [PATCH 23/39] AtlasEngine: Implement remaining underlines and builtin glyphs for D2D (#17278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implements builtin glyphs for our Direct2D renderer, as well as dashed and curly underlines. With this in place the only two features it doesn't support are inverted cursors and VT soft fonts. This allows us to remove the `_hack*` members introduced in a6a0e44. The implementation of dashed underlines is trivial, while curly underlines use quadratic bezier curves. Caching the curve as a sprite is possible, however I feel like that can be done in the future. Builtin glyphs on the other hand require a cache, because otherwise filling the entire viewport with shaded glyphs would result in poor performance. This is why it's built on top of `ID2D1SpriteBatch`. Unfortunately the API causes an eager flush of other pending graphics instructions, which is why there's still a decent perf hit. Finally, as a little extra, this fixes the rounded powerline glyph shapes being slightly cut off. The fix is to simply don't round the position and radius of the ellipsis/semi-circle. Closes #17224 ## Validation Steps Performed * RenderingTests.exe updated ✅ * All supported builtin glyphs look sorta right at different sizes ✅ --- src/renderer/atlas/AtlasEngine.cpp | 14 +- src/renderer/atlas/AtlasEngine.h | 12 - src/renderer/atlas/AtlasEngine.r.cpp | 8 +- src/renderer/atlas/BackendD2D.cpp | 370 ++++++++++++++++++++++----- src/renderer/atlas/BackendD2D.h | 15 +- src/renderer/atlas/BackendD3D.cpp | 32 ++- src/renderer/atlas/BuiltinGlyphs.cpp | 59 +++-- src/renderer/atlas/BuiltinGlyphs.h | 12 +- src/tools/RenderingTests/main.cpp | 60 ++--- 9 files changed, 422 insertions(+), 160 deletions(-) diff --git a/src/renderer/atlas/AtlasEngine.cpp b/src/renderer/atlas/AtlasEngine.cpp index 28295aeadd2..b5a89f6c09d 100644 --- a/src/renderer/atlas/AtlasEngine.cpp +++ b/src/renderer/atlas/AtlasEngine.cpp @@ -74,9 +74,8 @@ try _handleSettingsUpdate(); } - if (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION || _hackTriggerRedrawAll) + if constexpr (ATLAS_DEBUG_DISABLE_PARTIAL_INVALIDATION) { - _hackTriggerRedrawAll = false; _api.invalidatedRows = invalidatedRowsAll; _api.scrollOffset = 0; } @@ -703,8 +702,6 @@ void AtlasEngine::_recreateFontDependentResources() _api.textFormatAxes[i] = { fontAxisValues.data(), fontAxisValues.size() }; } } - - _hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D; } void AtlasEngine::_recreateCellCountDependentResources() @@ -765,18 +762,13 @@ void AtlasEngine::_flushBufferLine() // This would seriously blow us up otherwise. Expects(_api.bufferLineColumn.size() == _api.bufferLine.size() + 1); + const auto builtinGlyphs = _p.s->font->builtinGlyphs; const auto beg = _api.bufferLine.data(); const auto len = _api.bufferLine.size(); size_t segmentBeg = 0; size_t segmentEnd = 0; bool custom = false; - if (!_hackWantsBuiltinGlyphs) - { - _mapRegularText(0, len); - return; - } - while (segmentBeg < len) { segmentEnd = segmentBeg; @@ -789,7 +781,7 @@ void AtlasEngine::_flushBufferLine() codepoint = til::combine_surrogates(codepoint, beg[i++]); } - const auto c = BuiltinGlyphs::IsBuiltinGlyph(codepoint) || BuiltinGlyphs::IsSoftFontChar(codepoint); + const auto c = (builtinGlyphs && BuiltinGlyphs::IsBuiltinGlyph(codepoint)) || BuiltinGlyphs::IsSoftFontChar(codepoint); if (custom != c) { break; diff --git a/src/renderer/atlas/AtlasEngine.h b/src/renderer/atlas/AtlasEngine.h index 1f26644ebe6..ccb4da9fb4e 100644 --- a/src/renderer/atlas/AtlasEngine.h +++ b/src/renderer/atlas/AtlasEngine.h @@ -127,18 +127,6 @@ namespace Microsoft::Console::Render::Atlas std::unique_ptr _b; RenderingPayload _p; - // _p.s->font->builtinGlyphs is the setting which decides whether we should map box drawing glyphs to - // our own builtin versions. There's just one problem: BackendD2D doesn't have this functionality. - // But since AtlasEngine shapes the text before it's handed to the backends, it would need to know - // whether BackendD2D is in use, before BackendD2D even exists. These two flags solve the issue - // by triggering a complete, immediate redraw whenever the backend type changes. - // - // The proper solution is to move text shaping into the backends. - // Someone just needs to write a generic "TextBuffer to DWRITE_GLYPH_RUN" function. - bool _hackIsBackendD2D = false; - bool _hackWantsBuiltinGlyphs = true; - bool _hackTriggerRedrawAll = false; - struct ApiState { GenerationalSettings s = DirtyGenerationalSettings(); diff --git a/src/renderer/atlas/AtlasEngine.r.cpp b/src/renderer/atlas/AtlasEngine.r.cpp index a0dbdcc5470..3591abe7905 100644 --- a/src/renderer/atlas/AtlasEngine.r.cpp +++ b/src/renderer/atlas/AtlasEngine.r.cpp @@ -77,7 +77,7 @@ CATCH_RETURN() [[nodiscard]] bool AtlasEngine::RequiresContinuousRedraw() noexcept { - return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw()) || _hackTriggerRedrawAll; + return ATLAS_DEBUG_CONTINUOUS_REDRAW || (_b && _b->RequiresContinuousRedraw()); } void AtlasEngine::WaitUntilCanRender() noexcept @@ -282,21 +282,15 @@ void AtlasEngine::_recreateBackend() { case GraphicsAPI::Direct2D: _b = std::make_unique(); - _hackIsBackendD2D = true; break; default: _b = std::make_unique(_p); - _hackIsBackendD2D = false; break; } // This ensures that the backends redraw their entire viewports whenever a new swap chain is created, // EVEN IF we got called when no actual settings changed (i.e. rendering failure, etc.). _p.MarkAllAsDirty(); - - const auto hackWantsBuiltinGlyphs = _p.s->font->builtinGlyphs && !_hackIsBackendD2D; - _hackTriggerRedrawAll = _hackWantsBuiltinGlyphs != hackWantsBuiltinGlyphs; - _hackWantsBuiltinGlyphs = hackWantsBuiltinGlyphs; } void AtlasEngine::_handleSwapChainUpdate() diff --git a/src/renderer/atlas/BackendD2D.cpp b/src/renderer/atlas/BackendD2D.cpp index 5a663b49d04..cf7bd8a2bec 100644 --- a/src/renderer/atlas/BackendD2D.cpp +++ b/src/renderer/atlas/BackendD2D.cpp @@ -4,6 +4,8 @@ #include "pch.h" #include "BackendD2D.h" +#include + #if ATLAS_DEBUG_SHOW_DIRTY #include "colorbrewer.h" #endif @@ -94,11 +96,15 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) .dpiY = static_cast(p.s->font->dpi), }; // ID2D1RenderTarget and ID2D1DeviceContext are the same and I'm tired of pretending they're not. - THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_renderTarget.addressof()))); - _renderTarget.try_query_to(_renderTarget4.addressof()); + THROW_IF_FAILED(p.d2dFactory->CreateDxgiSurfaceRenderTarget(surface.get(), &props, reinterpret_cast(_renderTarget.put()))); _renderTarget->SetUnitMode(D2D1_UNIT_MODE_PIXELS); - _renderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + + _renderTarget.try_query_to(_renderTarget4.put()); + if (_renderTarget4) + { + THROW_IF_FAILED(_renderTarget4->CreateSpriteBatch(_builtinGlyphBatch.put())); + } } { static constexpr D2D1_COLOR_F color{}; @@ -108,18 +114,15 @@ void BackendD2D::_handleSettingsUpdate(const RenderingPayload& p) } } - if (!_dottedStrokeStyle) - { - static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; - static constexpr FLOAT dashes[2]{ 1, 1 }; - THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof())); - } - if (renderTargetChanged || fontChanged) { const auto dpi = static_cast(p.s->font->dpi); _renderTarget->SetDpi(dpi, dpi); _renderTarget->SetTextAntialiasMode(static_cast(p.s->font->antialiasingMode)); + + _builtinGlyphsRenderTarget.reset(); + _builtinGlyphsBitmap.reset(); + _builtinGlyphsRenderTargetActive = false; } if (renderTargetChanged || fontChanged || cellCountChanged) @@ -199,6 +202,12 @@ void BackendD2D::_drawText(RenderingPayload& p) for (const auto& m : row->mappings) { + if (!m.fontFace) + { + baselineX = _drawBuiltinGlyphs(p, row, m, baselineY, baselineX); + continue; + } + const auto colorsBegin = row->colors.begin(); auto it = colorsBegin + m.glyphsFrom; const auto end = colorsBegin + m.glyphsTo; @@ -228,42 +237,39 @@ void BackendD2D::_drawText(RenderingPayload& p) baselineY, }; - if (glyphRun.fontFace) + D2D1_RECT_F bounds = GlyphRunEmptyBounds; + wil::com_ptr enumerator; + + if (p.s->font->colorGlyphs) { - D2D1_RECT_F bounds = GlyphRunEmptyBounds; - wil::com_ptr enumerator; + enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun); + } - if (p.s->font->colorGlyphs) + if (enumerator) + { + while (ColorGlyphRunMoveNext(enumerator.get())) { - enumerator = TranslateColorGlyphRun(p.dwriteFactory4.get(), baselineOrigin, &glyphRun); + const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); + ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun); + ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds); } + } + else + { + _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL); + GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds); + } - if (enumerator) - { - while (ColorGlyphRunMoveNext(enumerator.get())) - { - const auto colorGlyphRun = ColorGlyphRunGetCurrentRun(enumerator.get()); - ColorGlyphRunDraw(_renderTarget4.get(), _emojiBrush.get(), brush, colorGlyphRun); - ColorGlyphRunAccumulateBounds(_renderTarget.get(), colorGlyphRun, bounds); - } - } - else + if (bounds.top < bounds.bottom) + { + // Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding. + if (row->lineRendition != LineRendition::DoubleHeightTop) { - _renderTarget->DrawGlyphRun(baselineOrigin, &glyphRun, brush, DWRITE_MEASURING_MODE_NATURAL); - GlyphRunAccumulateBounds(_renderTarget.get(), baselineOrigin, &glyphRun, bounds); + row->dirtyBottom = std::max(row->dirtyBottom, static_cast(lrintf(bounds.bottom))); } - - if (bounds.top < bounds.bottom) + if (row->lineRendition != LineRendition::DoubleHeightBottom) { - // Since we used SetUnitMode(D2D1_UNIT_MODE_PIXELS), bounds.top/bottom is in pixels already and requires no conversion/rounding. - if (row->lineRendition != LineRendition::DoubleHeightTop) - { - row->dirtyBottom = std::max(row->dirtyBottom, static_cast(lrintf(bounds.bottom))); - } - if (row->lineRendition != LineRendition::DoubleHeightBottom) - { - row->dirtyTop = std::min(row->dirtyTop, static_cast(lrintf(bounds.top))); - } + row->dirtyTop = std::min(row->dirtyTop, static_cast(lrintf(bounds.top))); } } @@ -274,6 +280,8 @@ void BackendD2D::_drawText(RenderingPayload& p) } } + _flushBuiltinGlyphs(); + if (!row->gridLineRanges.empty()) { _drawGridlineRow(p, row, y); @@ -300,6 +308,160 @@ void BackendD2D::_drawText(RenderingPayload& p) } } +f32 BackendD2D::_drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* row, const FontMapping& m, f32 baselineY, f32 baselineX) +{ + const f32 cellTop = baselineY - p.s->font->baseline; + const f32 cellBottom = cellTop + p.s->font->cellSize.y; + const f32 cellWidth = p.s->font->cellSize.x; + + _prepareBuiltinGlyphRenderTarget(p); + + for (size_t i = m.glyphsFrom; i < m.glyphsTo; ++i) + { + // This code runs when fontFace == nullptr. This is only the case for builtin glyphs which then use the glyphIndices + // to store UTF16 code points. In other words, this doesn't accidentally corrupt any actual glyph indices. + u32 ch = row->glyphIndices[i]; + if (til::is_leading_surrogate(ch)) + { + i += 1; + ch = til::combine_surrogates(ch, row->glyphIndices[i]); + } + + // If we don't have support for ID2D1SpriteBatch we don't support builtin glyphs. + // But we do still need to account for the glyphAdvances, which is why we can't just skip everything. + // It's very unlikely for a target device to not support ID2D1SpriteBatch as it's very old at this point. + if (_builtinGlyphBatch) + { + if (const auto off = BuiltinGlyphs::GetBitmapCellIndex(ch); off >= 0) + { + const D2D1_RECT_F dst{ baselineX, cellTop, baselineX + cellWidth, cellBottom }; + const auto src = _prepareBuiltinGlyph(p, ch, off); + const auto color = colorFromU32(row->colors[i]); + THROW_IF_FAILED(_builtinGlyphBatch->AddSprites(1, &dst, &src, &color, nullptr, sizeof(D2D1_RECT_F), sizeof(D2D1_RECT_U), sizeof(D2D1_COLOR_F), sizeof(D2D1_MATRIX_3X2_F))); + } + } + + baselineX += row->glyphAdvances[i]; + } + + return baselineX; +} + +void BackendD2D::_prepareBuiltinGlyphRenderTarget(const RenderingPayload& p) +{ + // If we don't have support for ID2D1SpriteBatch none of the related members will be initialized or used. + // We can just early-return in that case. + if (!_builtinGlyphBatch) + { + return; + } + + // If the render target is already created, all of the below has already been done in a previous frame. + // Once the relevant settings change for some reason (primarily the font->cellSize), then _handleSettingsUpdate() + // will reset the render target which will cause us to skip this condition and re-initialize it below. + if (_builtinGlyphsRenderTarget) + { + return; + } + + const auto cellWidth = static_cast(p.s->font->cellSize.x); + const auto cellHeight = static_cast(p.s->font->cellSize.y); + const auto cellArea = cellWidth * cellHeight; + const auto area = cellArea * BuiltinGlyphs::TotalCharCount; + + // This block of code calculates the size of a power-of-2 texture that has an area larger than the given `area`. + // For instance, for an area of 985x1946 = 1916810 it would result in a u/v of 2048x1024 (area = 2097152). + // We throw the "v" in this case away, because we don't really need power-of-2 textures here, + // but you can find the complete code over in BackendD3D. If someone deleted it in the meantime: + // const auto index = bitness_of_area_minus_1 - std::countl_zero(area - 1); // aka: _BitScanReverse + // const auto u = 1u << ((index + 2) / 2); + // const auto v = 1u << ((index + 1) / 2); + unsigned long index; + _BitScanReverse(&index, area - 1); + const auto potWidth = 1u << ((index + 2) / 2); + + const auto cellCountU = potWidth / cellWidth; + const auto cellCountV = (BuiltinGlyphs::TotalCharCount + cellCountU - 1) / cellCountU; + const auto u = cellCountU * cellWidth; + const auto v = cellCountV * cellHeight; + + const D2D1_SIZE_F sizeF{ static_cast(u), static_cast(v) }; + const D2D1_SIZE_U sizeU{ gsl::narrow_cast(u), gsl::narrow_cast(v) }; + static constexpr D2D1_PIXEL_FORMAT format{ DXGI_FORMAT_A8_UNORM, D2D1_ALPHA_MODE_PREMULTIPLIED }; + wil::com_ptr target; + THROW_IF_FAILED(_renderTarget->CreateCompatibleRenderTarget(&sizeF, &sizeU, &format, D2D1_COMPATIBLE_RENDER_TARGET_OPTIONS_NONE, target.addressof())); + + THROW_IF_FAILED(target->GetBitmap(_builtinGlyphsBitmap.put())); + _builtinGlyphsRenderTarget = target.query(); + _builtinGlyphsBitmapCellCountU = cellCountU; + _builtinGlyphsRenderTargetActive = false; + memset(&_builtinGlyphsReady[0], 0, sizeof(_builtinGlyphsReady)); +} + +D2D1_RECT_U BackendD2D::_prepareBuiltinGlyph(const RenderingPayload& p, char32_t ch, u32 off) +{ + const u32 w = p.s->font->cellSize.x; + const u32 h = p.s->font->cellSize.y; + const u32 l = (off % _builtinGlyphsBitmapCellCountU) * w; + const u32 t = (off / _builtinGlyphsBitmapCellCountU) * h; + D2D1_RECT_U rectU{ l, t, l + w, t + h }; + + // Check if we previously cached this glyph already. + if (_builtinGlyphsReady[off]) + { + return rectU; + } + + static constexpr D2D1_COLOR_F shadeColorMap[] = { + { 1, 1, 1, 0.25f }, // Shape_Filled025 + { 1, 1, 1, 0.50f }, // Shape_Filled050 + { 1, 1, 1, 0.75f }, // Shape_Filled075 + { 1, 1, 1, 1.00f }, // Shape_Filled100 + }; + + if (!_builtinGlyphsRenderTargetActive) + { + _builtinGlyphsRenderTarget->BeginDraw(); + _builtinGlyphsRenderTargetActive = true; + } + + const auto brush = _brushWithColor(0xffffffff); + const D2D1_RECT_F rectF{ + static_cast(rectU.left), + static_cast(rectU.top), + static_cast(rectU.right), + static_cast(rectU.bottom), + }; + BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _builtinGlyphsRenderTarget.get(), brush, shadeColorMap, rectF, ch); + + _builtinGlyphsReady[off] = true; + return rectU; +} + +void BackendD2D::_flushBuiltinGlyphs() +{ + // If we don't have support for ID2D1SpriteBatch none of the related members will be initialized or used. + // We can just early-return in that case. + if (!_builtinGlyphBatch) + { + return; + } + + if (_builtinGlyphsRenderTargetActive) + { + THROW_IF_FAILED(_builtinGlyphsRenderTarget->EndDraw()); + _builtinGlyphsRenderTargetActive = false; + } + + if (const auto count = _builtinGlyphBatch->GetSpriteCount(); count > 0) + { + _renderTarget4->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED); + _renderTarget4->DrawSpriteBatch(_builtinGlyphBatch.get(), 0, count, _builtinGlyphsBitmap.get(), D2D1_BITMAP_INTERPOLATION_MODE_NEAREST_NEIGHBOR, D2D1_SPRITE_OPTIONS_NONE); + _renderTarget4->SetAntialiasMode(D2D1_ANTIALIAS_MODE_PER_PRIMITIVE); + _builtinGlyphBatch->Clear(); + } +} + f32 BackendD2D::_drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept { const auto lineRendition = row->lineRendition; @@ -410,44 +572,118 @@ f32r BackendD2D::_getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* row, u16 y) { - const auto widthShift = gsl::narrow_cast(row->lineRendition != LineRendition::SingleWidth); - const auto cellSize = p.s->font->cellSize; - const auto rowTop = gsl::narrow_cast(cellSize.y * y); - const auto rowBottom = gsl::narrow_cast(rowTop + cellSize.y); - const auto textCellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop; + const auto cellWidth = static_cast(p.s->font->cellSize.x); + const auto cellHeight = static_cast(p.s->font->cellSize.y); + const auto rowTop = cellHeight * y; + const auto rowBottom = rowTop + cellHeight; + const auto cellCenter = row->lineRendition == LineRendition::DoubleHeightTop ? rowBottom : rowTop; + const auto scaleHorizontal = row->lineRendition != LineRendition::SingleWidth ? 0.5f : 1.0f; + const auto scaledCellWidth = cellWidth * scaleHorizontal; const auto appendVerticalLines = [&](const GridLineRange& r, FontDecorationPosition pos) { - const auto from = r.from >> widthShift; - const auto to = r.to >> widthShift; + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; + auto x = from + pos.position; - auto posX = from * cellSize.x + pos.position; - const auto end = to * cellSize.x; - - D2D1_POINT_2F point0{ 0, static_cast(textCellCenter) }; - D2D1_POINT_2F point1{ 0, static_cast(textCellCenter + cellSize.y) }; + D2D1_POINT_2F point0{ 0, cellCenter }; + D2D1_POINT_2F point1{ 0, cellCenter + cellHeight }; const auto brush = _brushWithColor(r.gridlineColor); const f32 w = pos.height; const f32 hw = w * 0.5f; - for (; posX < end; posX += cellSize.x) + for (; x < to; x += cellWidth) { - const auto centerX = posX + hw; + const auto centerX = x + hw; point0.x = centerX; point1.x = centerX; _renderTarget->DrawLine(point0, point1, brush, w, nullptr); } }; const auto appendHorizontalLine = [&](const GridLineRange& r, FontDecorationPosition pos, ID2D1StrokeStyle* strokeStyle, const u32 color) { - const auto from = r.from >> widthShift; - const auto to = r.to >> widthShift; + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; const auto brush = _brushWithColor(color); const f32 w = pos.height; - const f32 centerY = textCellCenter + pos.position + w * 0.5f; - const D2D1_POINT_2F point0{ static_cast(from * cellSize.x), centerY }; - const D2D1_POINT_2F point1{ static_cast(to * cellSize.x), centerY }; + const f32 centerY = cellCenter + pos.position + w * 0.5f; + const D2D1_POINT_2F point0{ from, centerY }; + const D2D1_POINT_2F point1{ to, centerY }; _renderTarget->DrawLine(point0, point1, brush, w, strokeStyle); }; + const auto appendCurlyLine = [&](const GridLineRange& r) { + const auto& font = *p.s->font; + + const auto duTop = static_cast(font.doubleUnderline[0].position); + const auto duBottom = static_cast(font.doubleUnderline[1].position); + // The double-underline height is also our target line width. + const auto duHeight = static_cast(font.doubleUnderline[0].height); + + // This gives it the same position and height as our double-underline. There's no particular reason for that, apart from + // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc. + // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min(). + const auto height = std::max(3.0f, duBottom + duHeight - duTop); + const auto position = std::min(duTop, cellHeight - height - duHeight); + + // The amplitude of the wave needs to account for the stroke width, so that the final height including + // antialiasing isn't larger than our target `height`. That's why we calculate `(height - duHeight)`. + // + // In other words, Direct2D draws strokes centered on the path. This also means that (for instance) + // for a line width of 1px, we need to ensure that the amplitude passes through the center of a pixel. + // Because once the path gets stroked, it'll occupy half a pixel on either side of the path. + // This results in a "crisp" look. That's why we do `round(amp + half) - half`. + const auto halfLineWidth = 0.5f * duHeight; + const auto amplitude = roundf((height - duHeight) * 0.5f + halfLineWidth) - halfLineWidth; + // While the amplitude needs to account for the stroke width, the vertical center of the wave needs + // to be at an integer pixel position of course. Otherwise, the wave won't be vertically symmetric. + const auto center = cellCenter + position + amplitude + halfLineWidth; + + const auto top = center - 2.0f * amplitude; + const auto bottom = center + 2.0f * amplitude; + const auto step = 0.5f * height; + const auto period = 4.0f * step; + + const auto from = r.from * scaledCellWidth; + const auto to = r.to * scaledCellWidth; + // Align the start of the wave to the nearest preceding period boundary. + // This ensures that the wave is continuous across color and cell changes. + auto x = floorf(from / period) * period; + + wil::com_ptr geometry; + THROW_IF_FAILED(p.d2dFactory->CreatePathGeometry(geometry.addressof())); + + wil::com_ptr sink; + THROW_IF_FAILED(geometry->Open(sink.addressof())); + + // This adds complete periods of the wave until we reach the end of the range. + sink->BeginFigure({ x, center }, D2D1_FIGURE_BEGIN_HOLLOW); + for (D2D1_QUADRATIC_BEZIER_SEGMENT segment; x < to;) + { + x += step; + segment.point1.x = x; + segment.point1.y = top; + x += step; + segment.point2.x = x; + segment.point2.y = center; + sink->AddQuadraticBezier(&segment); + + x += step; + segment.point1.x = x; + segment.point1.y = bottom; + x += step; + segment.point2.x = x; + segment.point2.y = center; + sink->AddQuadraticBezier(&segment); + } + sink->EndFigure(D2D1_FIGURE_END_OPEN); + + THROW_IF_FAILED(sink->Close()); + + const auto brush = _brushWithColor(r.underlineColor); + const D2D1_RECT_F clipRect{ from, rowTop, to, rowBottom }; + _renderTarget->PushAxisAlignedClip(&clipRect, D2D1_ANTIALIAS_MODE_ALIASED); + _renderTarget->DrawGeometry(geometry.get(), brush, duHeight, nullptr); + _renderTarget->PopAxisAlignedClip(); + }; for (const auto& r : row->gridLineRanges) { @@ -481,8 +717,28 @@ void BackendD2D::_drawGridlineRow(const RenderingPayload& p, const ShapedRow* ro } else if (r.lines.any(GridLines::DottedUnderline, GridLines::HyperlinkUnderline)) { + if (!_dottedStrokeStyle) + { + static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; + static constexpr FLOAT dashes[2]{ 1, 1 }; + THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dottedStrokeStyle.addressof())); + } appendHorizontalLine(r, p.s->font->underline, _dottedStrokeStyle.get(), r.underlineColor); } + else if (r.lines.test(GridLines::DashedUnderline)) + { + if (!_dashedStrokeStyle) + { + static constexpr D2D1_STROKE_STYLE_PROPERTIES props{ .dashStyle = D2D1_DASH_STYLE_CUSTOM }; + static constexpr FLOAT dashes[2]{ 2, 2 }; + THROW_IF_FAILED(p.d2dFactory->CreateStrokeStyle(&props, &dashes[0], 2, _dashedStrokeStyle.addressof())); + } + appendHorizontalLine(r, p.s->font->underline, _dashedStrokeStyle.get(), r.underlineColor); + } + else if (r.lines.test(GridLines::CurlyUnderline)) + { + appendCurlyLine(r); + } else if (r.lines.test(GridLines::DoubleUnderline)) { for (const auto pos : p.s->font->doubleUnderline) diff --git a/src/renderer/atlas/BackendD2D.h b/src/renderer/atlas/BackendD2D.h index e6993d60603..4206390ea52 100644 --- a/src/renderer/atlas/BackendD2D.h +++ b/src/renderer/atlas/BackendD2D.h @@ -3,9 +3,8 @@ #pragma once -#include - #include "Backend.h" +#include "BuiltinGlyphs.h" namespace Microsoft::Console::Render::Atlas { @@ -19,6 +18,10 @@ namespace Microsoft::Console::Render::Atlas ATLAS_ATTR_COLD void _handleSettingsUpdate(const RenderingPayload& p); void _drawBackground(const RenderingPayload& p); void _drawText(RenderingPayload& p); + ATLAS_ATTR_COLD f32 _drawBuiltinGlyphs(const RenderingPayload& p, const ShapedRow* row, const FontMapping& m, f32 baselineY, f32 baselineX); + void _prepareBuiltinGlyphRenderTarget(const RenderingPayload& p); + D2D1_RECT_U _prepareBuiltinGlyph(const RenderingPayload& p, char32_t ch, u32 off); + void _flushBuiltinGlyphs(); ATLAS_ATTR_COLD f32 _drawTextPrepareLineRendition(const RenderingPayload& p, const ShapedRow* row, f32 baselineY) const noexcept; ATLAS_ATTR_COLD void _drawTextResetLineRendition(const ShapedRow* row) const noexcept; ATLAS_ATTR_COLD f32r _getGlyphRunDesignBounds(const DWRITE_GLYPH_RUN& glyphRun, f32 baselineX, f32 baselineY); @@ -37,10 +40,18 @@ namespace Microsoft::Console::Render::Atlas wil::com_ptr _renderTarget; wil::com_ptr _renderTarget4; // Optional. Supported since Windows 10 14393. wil::com_ptr _dottedStrokeStyle; + wil::com_ptr _dashedStrokeStyle; wil::com_ptr _backgroundBitmap; wil::com_ptr _backgroundBrush; til::generation_t _backgroundBitmapGeneration; + wil::com_ptr _builtinGlyphsRenderTarget; + wil::com_ptr _builtinGlyphsBitmap; + wil::com_ptr _builtinGlyphBatch; + u32 _builtinGlyphsBitmapCellCountU = 0; + bool _builtinGlyphsRenderTargetActive = false; + bool _builtinGlyphsReady[BuiltinGlyphs::TotalCharCount]{}; + wil::com_ptr _cursorBitmap; til::size _cursorBitmapSize; // in columns/rows diff --git a/src/renderer/atlas/BackendD3D.cpp b/src/renderer/atlas/BackendD3D.cpp index fe07271c16a..d85d6e0365f 100644 --- a/src/renderer/atlas/BackendD3D.cpp +++ b/src/renderer/atlas/BackendD3D.cpp @@ -295,20 +295,20 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p) // baseline of curlyline is at the middle of singly underline. When there's // limited space to draw a curlyline, we apply a limit on the peak height. { - const auto cellHeight = static_cast(font.cellSize.y); - const auto duTop = static_cast(font.doubleUnderline[0].position); - const auto duBottom = static_cast(font.doubleUnderline[1].position); - const auto duHeight = static_cast(font.doubleUnderline[0].height); + const int cellHeight = font.cellSize.y; + const int duTop = font.doubleUnderline[0].position; + const int duBottom = font.doubleUnderline[1].position; + const int duHeight = font.doubleUnderline[0].height; // This gives it the same position and height as our double-underline. There's no particular reason for that, apart from // it being simple to implement and robust against more peculiar fonts with unusually large/small descenders, etc. - // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom. - const auto height = std::max(3.0f, duBottom + duHeight - duTop); - const auto top = std::min(duTop, floorf(cellHeight - height - duHeight)); + // We still need to ensure though that it doesn't clip out of the cellHeight at the bottom, which is why `position` has a min(). + const auto height = std::max(3, duBottom + duHeight - duTop); + const auto position = std::min(duTop, cellHeight - height - duHeight); _curlyLineHalfHeight = height * 0.5f; - _curlyUnderline.position = gsl::narrow_cast(lrintf(top)); - _curlyUnderline.height = gsl::narrow_cast(lrintf(height)); + _curlyUnderline.position = gsl::narrow_cast(position); + _curlyUnderline.height = gsl::narrow_cast(height); } DWrite_GetRenderParams(p.dwriteFactory.get(), &_gamma, &_cleartypeEnhancedContrast, &_grayscaleEnhancedContrast, _textRenderingParams.put()); @@ -1509,7 +1509,19 @@ BackendD3D::AtlasGlyphEntry* BackendD3D::_drawBuiltinGlyph(const RenderingPayloa } else { - BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), r, glyphIndex); + // This code works in tandem with SHADING_TYPE_TEXT_BUILTIN_GLYPH in our pixel shader. + // Unless someone removed it, it should have a lengthy comment visually explaining + // what each of the 3 RGB components do. The short version is: + // R: stretch the checkerboard pattern (Shape_Filled050) horizontally + // G: invert the pixels + // B: overrides the above and fills it + static constexpr D2D1_COLOR_F shadeColorMap[] = { + { 1, 0, 0, 1 }, // Shape_Filled025 + { 0, 0, 0, 1 }, // Shape_Filled050 + { 1, 1, 0, 1 }, // Shape_Filled075 + { 1, 1, 1, 1 }, // Shape_Filled100 + }; + BuiltinGlyphs::DrawBuiltinGlyph(p.d2dFactory.get(), _d2dRenderTarget.get(), _brush.get(), shadeColorMap, r, glyphIndex); shadingType = ShadingType::TextBuiltinGlyph; } diff --git a/src/renderer/atlas/BuiltinGlyphs.cpp b/src/renderer/atlas/BuiltinGlyphs.cpp index c46bfc01fc0..92d87bc0b45 100644 --- a/src/renderer/atlas/BuiltinGlyphs.cpp +++ b/src/renderer/atlas/BuiltinGlyphs.cpp @@ -135,8 +135,6 @@ inline constexpr f32 Pos_Lut[][2] = { /* Pos_11_12 */ { 11.0f / 12.0f, 0.0f }, }; -static constexpr char32_t BoxDrawing_FirstChar = 0x2500; -static constexpr u32 BoxDrawing_CharCount = 0xA0; static constexpr Instruction BoxDrawing[BoxDrawing_CharCount][InstructionsPerGlyph] = { // U+2500 ─ BOX DRAWINGS LIGHT HORIZONTAL { @@ -964,8 +962,6 @@ static constexpr Instruction BoxDrawing[BoxDrawing_CharCount][InstructionsPerGly }, }; -static constexpr char32_t Powerline_FirstChar = 0xE0B0; -static constexpr u32 Powerline_CharCount = 0x10; static constexpr Instruction Powerline[Powerline_CharCount][InstructionsPerGlyph] = { // U+E0B0 Right triangle solid { @@ -1071,7 +1067,20 @@ static const Instruction* GetInstructions(char32_t codepoint) noexcept return nullptr; } -void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint) +i32 BuiltinGlyphs::GetBitmapCellIndex(char32_t codepoint) noexcept +{ + if (BoxDrawing_IsMapped(codepoint)) + { + return codepoint - BoxDrawing_FirstChar; + } + if (Powerline_IsMapped(codepoint)) + { + return codepoint - Powerline_FirstChar + BoxDrawing_CharCount; + } + return -1; +} + +void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_COLOR_F (&shadeColorMap)[4], const D2D1_RECT_F& rect, char32_t codepoint) { renderTarget->PushAxisAlignedClip(&rect, D2D1_ANTIALIAS_MODE_ALIASED); const auto restoreD2D = wil::scope_exit([&]() { @@ -1122,15 +1131,18 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* const auto lineOffsetX = isHollowRect || isLineX ? lineWidthHalf : 0.0f; const auto lineOffsetY = isHollowRect || isLineY ? lineWidthHalf : 0.0f; - begX = roundf(begX - lineOffsetX) + lineOffsetX; - begY = roundf(begY - lineOffsetY) + lineOffsetY; - endX = roundf(endX + lineOffsetX) - lineOffsetX; - endY = roundf(endY + lineOffsetY) - lineOffsetY; - - const auto begXabs = begX + rectX; - const auto begYabs = begY + rectY; - const auto endXabs = endX + rectX; - const auto endYabs = endY + rectY; + // Direct2D draws strokes centered on the path. In order to make them pixel-perfect we need to round the + // coordinates to whole pixels, but offset by half the stroke width (= the radius of the stroke). + // + // All floats up to this point will be highly "consistent" between different `rect`s of identical size and + // different shapes, because the above calculations work with only a small set of constant floats. + // However, the addition of a potentially fractional begX/Y with a highly variable `rect` position is different. + // Rounding beg/endX/Y first ensures that we continue to get a consistent behavior between calls. + // This is particularly noticeable at smaller font sizes, where the line width is just a pixel or two. + const auto begXabs = rectX + roundf(begX - lineOffsetX) + lineOffsetX; + const auto begYabs = rectY + roundf(begY - lineOffsetY) + lineOffsetY; + const auto endXabs = rectX + roundf(endX + lineOffsetX) - lineOffsetX; + const auto endYabs = rectY + roundf(endY + lineOffsetY) - lineOffsetY; switch (shape) { @@ -1139,21 +1151,8 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* case Shape_Filled075: case Shape_Filled100: { - // This code works in tandem with SHADING_TYPE_TEXT_BUILTIN_GLYPH in our pixel shader. - // Unless someone removed it, it should have a lengthy comment visually explaining - // what each of the 3 RGB components do. The short version is: - // R: stretch the checkerboard pattern (Shape_Filled050) horizontally - // G: invert the pixels - // B: overrides the above and fills it - static constexpr D2D1_COLOR_F colors[] = { - { 1, 0, 0, 1 }, // Shape_Filled025 - { 0, 0, 0, 1 }, // Shape_Filled050 - { 1, 1, 0, 1 }, // Shape_Filled075 - { 1, 1, 1, 1 }, // Shape_Filled100 - }; - const auto brushColor = brush->GetColor(); - brush->SetColor(&colors[shape]); + brush->SetColor(&shadeColorMap[shape]); const D2D1_RECT_F r{ begXabs, begYabs, endXabs, endYabs }; renderTarget->FillRectangle(&r, brush); @@ -1183,13 +1182,13 @@ void BuiltinGlyphs::DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* } case Shape_FilledEllipsis: { - const D2D1_ELLIPSE e{ { begXabs, begYabs }, endX, endY }; + const D2D1_ELLIPSE e{ { rectX + begX, rectY + begY }, endX, endY }; renderTarget->FillEllipse(&e, brush); break; } case Shape_EmptyEllipsis: { - const D2D1_ELLIPSE e{ { begXabs, begYabs }, endX, endY }; + const D2D1_ELLIPSE e{ { rectX + begX, rectY + begY }, endX, endY }; renderTarget->DrawEllipse(&e, brush, lineWidth, nullptr); break; } diff --git a/src/renderer/atlas/BuiltinGlyphs.h b/src/renderer/atlas/BuiltinGlyphs.h index f399653893c..b6cce3a3974 100644 --- a/src/renderer/atlas/BuiltinGlyphs.h +++ b/src/renderer/atlas/BuiltinGlyphs.h @@ -8,7 +8,17 @@ namespace Microsoft::Console::Render::Atlas::BuiltinGlyphs { bool IsBuiltinGlyph(char32_t codepoint) noexcept; - void DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_RECT_F& rect, char32_t codepoint); + void DrawBuiltinGlyph(ID2D1Factory* factory, ID2D1DeviceContext* renderTarget, ID2D1SolidColorBrush* brush, const D2D1_COLOR_F (&shadeColorMap)[4], const D2D1_RECT_F& rect, char32_t codepoint); + + inline constexpr char32_t BoxDrawing_FirstChar = 0x2500; + inline constexpr u32 BoxDrawing_CharCount = 0xA0; + + inline constexpr char32_t Powerline_FirstChar = 0xE0B0; + inline constexpr u32 Powerline_CharCount = 0x10; + + inline constexpr u32 TotalCharCount = BoxDrawing_CharCount + Powerline_CharCount; + + i32 GetBitmapCellIndex(char32_t codepoint) noexcept; // This is just an extra. It's not actually implemented as part of BuiltinGlyphs.cpp. constexpr bool IsSoftFontChar(char32_t ch) noexcept diff --git a/src/tools/RenderingTests/main.cpp b/src/tools/RenderingTests/main.cpp index 45dfcf66ea7..2d0ef8612a4 100644 --- a/src/tools/RenderingTests/main.cpp +++ b/src/tools/RenderingTests/main.cpp @@ -108,15 +108,15 @@ static void printfUTF16(_In_z_ _Printf_format_string_ wchar_t const* const forma static void wait() { - printUTF16(L"\x1B[9999;1HPress any key to continue..."); + printUTF16(L"\x1b[9999;1HPress any key to continue..."); _getch(); } static void clear() { printUTF16( - L"\x1B[H" // move cursor to 0,0 - L"\x1B[2J" // clear screen + L"\x1b[H" // move cursor to 0,0 + L"\x1b[2J" // clear screen ); } @@ -166,7 +166,7 @@ int main() for (const auto& t : consoleAttributeTests) { const auto length = static_cast(wcslen(t.text)); - printfUTF16(L"\x1B[%d;5H%s", row + 1, t.text); + printfUTF16(L"\x1b[%d;5H%s", row + 1, t.text); WORD attributes[32]; std::fill_n(&attributes[0], length, static_cast(FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | t.attribute)); @@ -190,16 +190,16 @@ int main() { L"overlined", 53 }, }; - printfUTF16(L"\x1B[3;39HANSI escape SGR:"); + printfUTF16(L"\x1b[3;39HANSI escape SGR:"); int row = 5; for (const auto& t : basicSGR) { - printfUTF16(L"\x1B[%d;39H\x1b[%dm%s\x1b[m", row, t.attribute, t.text); + printfUTF16(L"\x1b[%d;39H\x1b[%dm%s\x1b[m", row, t.attribute, t.text); row += 2; } - printfUTF16(L"\x1B[%d;39H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row); + printfUTF16(L"\x1b[%d;39H\x1b]8;;https://example.com\x1b\\hyperlink\x1b]8;;\x1b\\", row); } { @@ -211,18 +211,18 @@ int main() { L"dashed", 5 }, }; - printfUTF16(L"\x1B[3;63HStyled Underlines:"); + printfUTF16(L"\x1b[3;63HStyled Underlines:"); int row = 5; for (const auto& t : styledUnderlines) { - printfUTF16(L"\x1B[%d;63H\x1b[4:%dm", row, t.attribute); + printfUTF16(L"\x1b[%d;63H\x1b[4:%dm", row, t.attribute); const auto len = wcslen(t.text); for (size_t i = 0; i < len; ++i) { const auto color = colorbrewer::pastel1[i % std::size(colorbrewer::pastel1)]; - printfUTF16(L"\x1B[58:2::%d:%d:%dm%c", (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, t.text[i]); + printfUTF16(L"\x1b[58:2::%d:%d:%dm%c", (color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff, t.text[i]); } printfUTF16(L"\x1b[m"); @@ -236,19 +236,19 @@ int main() { printUTF16( - L"\x1B[3;5HDECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F" - L"\x1B[4;3H\x1b#6DECDWL Double Width \U0001FAE0 \x1B[45;92mA\u0353\u0353\x1B[m B\u036F\u036F" - L"\x1B[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F" - L"\x1B[8;3H\x1b#3DECDHL Double Height Top \U0001F952 A\u0353\u0353 \x1B[45;92mB\u036F\u036F\x1B[m" - L"\x1B[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1B[45;92mX\u0353\u0353\x1B[m Y\u036F\u036F" - L"\x1B[13;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[15;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" - L"\x1B[17;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[19;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" - L"\x1B[21;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[22;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1B[3mitalic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" - L"\x1B[24;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" - L"\x1B[25;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1B[45;92m!\x1B[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"); + L"\x1b[3;5HDECDWL Double Width \U0001FAE0 \x1b[45;92mA\u0353\u0353\x1b[m B\u036F\u036F" + L"\x1b[4;3H\x1b#6DECDWL Double Width \U0001FAE0 \x1b[45;92mA\u0353\u0353\x1b[m B\u036F\u036F" + L"\x1b[7;5HDECDHL Double Height \U0001F952\U0001F6C1 A\u0353\u0353 \x1b[45;92mB\u036F\u036F\x1b[m \x1b[45;92mX\u0353\u0353\x1b[m Y\u036F\u036F" + L"\x1b[8;3H\x1b#3DECDHL Double Height Top \U0001F952 A\u0353\u0353 \x1b[45;92mB\u036F\u036F\x1b[m" + L"\x1b[9;3H\x1b#4DECDHL Double Height Bottom \U0001F6C1 \x1b[45;92mX\u0353\u0353\x1b[m Y\u036F\u036F" + L"\x1b[12;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[14;5H\x1b]8;;https://example.com\x1b\\DECDxL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1b[16;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[18;3H\x1b#6\x1b]8;;https://vt100.net/docs/vt510-rm/DECDWL.html\x1b\\DECDWL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1b[20;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[21;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[3;4:3;58:2::255:0:0mita\x1b[58:2::0:255:0mlic\x1b[m \x1b[4munderline\x1b[m \x1b[7mreverse\x1b[m" + L"\x1b[23;3H\x1b#3\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m" + L"\x1b[24;3H\x1b#4\x1b]8;;https://vt100.net/docs/vt510-rm/DECDHL.html\x1b\\DECDHL\x1b]8;;\x1b\\ <\x1b[45;92m!\x1b[m-- \x1b[9mstrikethrough\x1b[m \x1b[21mdouble underline\x1b[m \x1b[53moverlined\x1b[m"); static constexpr WORD attributes[]{ FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED | COMMON_LVB_GRID_HORIZONTAL, @@ -264,7 +264,7 @@ int main() DWORD numberOfAttrsWritten; DWORD offset = 0; - for (const auto r : { 12, 14, 16, 18, 20, 21, 23, 24 }) + for (const auto r : { 11, 13, 15, 17, 19, 20, 22, 23 }) { COORD coord; coord.X = r > 14 ? 2 : 4; @@ -338,14 +338,14 @@ int main() #define DRCS_SEQUENCE L"\x1b( @#\x1b(A" printUTF16( - L"\x1B[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell" - L"\x1B[5;5HRegular: " DRCS_SEQUENCE L"" - L"\x1B[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L"" - L"\x1B[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L"" - L"\x1B[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L"" + L"\x1b[3;5HDECDLD and DRCS test - it should show \"WT\" in a single cell" + L"\x1b[5;5HRegular: " DRCS_SEQUENCE L"" + L"\x1b[7;3H\x1b#6DECDWL: " DRCS_SEQUENCE L"" + L"\x1b[9;3H\x1b#3DECDHL: " DRCS_SEQUENCE L"" + L"\x1b[10;3H\x1b#4DECDHL: " DRCS_SEQUENCE L"" // We map soft fonts into the private use area starting at U+EF20. This test ensures // that we correctly map actual fallback glyphs mixed into the DRCS glyphs. - L"\x1B[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004"); + L"\x1b[12;5HUnicode Fallback: \uE000\uE001" DRCS_SEQUENCE L"\uE003\uE004"); #undef DRCS_SEQUENCE wait(); From 26cc5da7f1dd860dab8bfe8595853dd674d4c463 Mon Sep 17 00:00:00 2001 From: Windows Console Service Bot <14666831+consvc@users.noreply.github.com> Date: Fri, 17 May 2024 12:42:09 -0500 Subject: [PATCH 24/39] Localization Updates - main - associated with #17259 (#17272) --- .../TerminalSettingsEditor/Resources/de-DE/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/es-ES/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/fr-FR/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/it-IT/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/ja-JP/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/ko-KR/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/pt-BR/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/qps-ploc/Resources.resw | 5 ++++- .../Resources/qps-ploca/Resources.resw | 5 ++++- .../Resources/qps-plocm/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/ru-RU/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/zh-CN/Resources.resw | 5 ++++- .../TerminalSettingsEditor/Resources/zh-TW/Resources.resw | 5 ++++- 13 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw index 27e9ace6da2..3cd573f25f4 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/de-DE/Resources.resw @@ -895,6 +895,9 @@ Schriftart Name for a control to select the font for text in the app. + + Sie können mehrere Schriftarten verwenden, indem Sie sie durch ein ASCII-Komma voneinander trennen. + Schriftgrad Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Wenn diese Option aktiviert ist, zeichnet das Terminal benutzerdefinierte Glyphen für Blockelement- und Feldzeichnungszeichen, anstatt die Schriftart zu verwenden. Dieses Feature funktioniert nur, wenn GPU-Beschleunigung verfügbar ist. + Wenn diese Option aktiviert ist, zeichnet das Terminal benutzerdefinierte Glyphen für Blockelement- und Feldzeichnungszeichen, anstatt die Schriftart zu verwenden. Dieses Feature ist nicht verfügbar, wenn Direct2D als Grafik-API verwendet wird. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw index 61ef4151625..49806cb4fb1 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/es-ES/Resources.resw @@ -895,6 +895,9 @@ Tipo de fuente Name for a control to select the font for text in the app. + + Puede usar varias fuentes separándolas con una coma ASCII. + Tamaño de la fuente Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Cuando está habilitado, el terminal dibuja glifos personalizados para los caracteres de dibujo de elementos de bloque y cuadros, en lugar de usar la fuente. Esta característica solo funciona cuando la aceleración de GPU está disponible. + Cuando está habilitado, el terminal dibuja glifos personalizados para los caracteres de dibujo de elementos de bloque y cuadros, en lugar de usar la fuente. Esta característica no está disponible cuando se usa Direct2D como API de gráficos. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw index 7f2a42f5f38..1c551a0699f 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/fr-FR/Resources.resw @@ -895,6 +895,9 @@ Type de police Name for a control to select the font for text in the app. + + Vous pouvez utiliser plusieurs polices en les séparant par une virgule ASCII. + Taille de police Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Lorsque cette option est activée, le terminal dessine des glyphes personnalisés pour les éléments de bloc et les caractères de dessin de boîte au lieu d’utiliser la police. Cette fonctionnalité ne fonctionne que lorsque l’accélération GPU est disponible. + Lorsque cette option est activée, le terminal dessine des glyphes personnalisés pour les éléments de bloc et les caractères de dessin de boîte au lieu d’utiliser la police. Cette fonctionnalité n’est pas disponible lors de l’utilisation de Direct2D comme API Graphics. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw index 1b2550d1d61..ca77707ca2f 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/it-IT/Resources.resw @@ -895,6 +895,9 @@ Tipo di carattere Name for a control to select the font for text in the app. + + Puoi usare più tipi di carattere separandoli con una virgola ASCII. + Dimensioni del carattere Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Se abilitata, il terminale disegna glifi personalizzati per i caratteri di disegno a blocchi di elementi e caselle anziché usare il tipo di carattere. Questa funzionalità funziona solo quando è disponibile l'accelerazione GPU. + Se abilitata, il terminale disegna glifi personalizzati per i caratteri di disegno a blocchi di elementi e caselle anziché usare il tipo di carattere. Questa funzionalità non è disponibile quando si usa Direct2D come API grafica. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw index 3ba41206daa..eca2e0bcf96 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ja-JP/Resources.resw @@ -895,6 +895,9 @@ フォント フェイス Name for a control to select the font for text in the app. + + 複数のフォントを使用するには、複数のフォントを ASCII コンマで区切ります。 + フォント サイズ Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - 有効にすると、ターミナルは、フォントを使用する代わりに、ブロック要素とボックス描画文字のカスタム グリフを描画します。この機能は、GPU アクセラレーションが使用可能な場合にのみ機能します。 + 有効にすると、ターミナルは、フォントを使用する代わりに、ブロック要素とボックス描画文字のカスタム グリフを描画します。グラフィックス API として Direct2D を使用している場合、この機能は使用できません。 A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw index 4e022bd43b8..976717c8677 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw @@ -895,6 +895,9 @@ 글꼴 Name for a control to select the font for text in the app. + + 여러 글꼴을 ASCII 쉼표로 구분하여 사용할 수 있습니다. + 글꼴 크기 Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - 사용하도록 설정하면 터미널은 글꼴을 사용하는 대신 블록 요소 및 상자 그리기 문자에 대한 사용자 지정 문자 모양을 그립니다. 이 기능은 GPU 가속을 사용할 수 있는 경우에만 작동합니다. + 사용하도록 설정하면 터미널은 글꼴을 사용하는 대신 블록 요소 및 상자 그리기 문자에 대한 사용자 지정 문자 모양을 그립니다. Direct2D를 그래픽 API로 사용할 때는 이 기능을 사용할 수 없습니다. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw index 8ac46dede93..ecdcef31cfc 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/pt-BR/Resources.resw @@ -895,6 +895,9 @@ Tipo de fonte Name for a control to select the font for text in the app. + + Pode utilizar vários tipos de letra separando-os com uma vírgula ASCII. + Tamanho da fonte Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Quando habilitado, o terminal desenha glifos personalizados para caracteres de desenho de elemento de bloco e caixa em vez de usar a fonte. Esse recurso só funciona quando a Aceleração de GPU está disponível. + Quando habilitado, o terminal desenha glifos personalizados para caracteres de desenho de elemento de bloco e caixa em vez de usar a fonte. Esse recurso não está disponível ao usar o Direct2D como a API de Gráficos. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw index a9e220a076c..666fff836bb 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploc/Resources.resw @@ -895,6 +895,9 @@ ₣øñŧ ƒдсέ !!! Name for a control to select the font for text in the app. + + Ýоύ ĉąи ύŝè мύŀŧΐρļĕ ƒőйτš ъý ѕéραгдŧĭʼnğ ţнэm щїţђ áп ∆ŚČĨĨ çőмmа. !!! !!! !!! !!! !!! !!! ! + ₣ŏňτ şίźε !!! Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè οʼnĺŷ ωóгκŝ ẁĥéŋ ĢΡŬ Àςĉеŀèѓдτισñ ĩѕ ǻνãîļåвłę. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! + Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè ίš úπâνάίļǻвĺз ẅнέи μşιńĝ Ďΐґеčт2Ď άѕ тħë Ĝяãрнĩςŝ ÃРÌ. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw index a9e220a076c..666fff836bb 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/qps-ploca/Resources.resw @@ -895,6 +895,9 @@ ₣øñŧ ƒдсέ !!! Name for a control to select the font for text in the app. + + Ýоύ ĉąи ύŝè мύŀŧΐρļĕ ƒőйτš ъý ѕéραгдŧĭʼnğ ţнэm щїţђ áп ∆ŚČĨĨ çőмmа. !!! !!! !!! !!! !!! !!! ! + ₣ŏňτ şίźε !!! Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè οʼnĺŷ ωóгκŝ ẁĥéŋ ĢΡŬ Àςĉеŀèѓдτισñ ĩѕ ǻνãîļåвłę. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! + Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè ίš úπâνάίļǻвĺз ẅнέи μşιńĝ Ďΐґеčт2Ď άѕ тħë Ĝяãрнĩςŝ ÃРÌ. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw index a9e220a076c..666fff836bb 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/qps-plocm/Resources.resw @@ -895,6 +895,9 @@ ₣øñŧ ƒдсέ !!! Name for a control to select the font for text in the app. + + Ýоύ ĉąи ύŝè мύŀŧΐρļĕ ƒőйτš ъý ѕéραгдŧĭʼnğ ţнэm щїţђ áп ∆ŚČĨĨ çőмmа. !!! !!! !!! !!! !!! !!! ! + ₣ŏňτ şίźε !!! Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè οʼnĺŷ ωóгκŝ ẁĥéŋ ĢΡŬ Àςĉеŀèѓдτισñ ĩѕ ǻνãîļåвłę. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! + Ẅħĕň еņªъℓёð, тћз τēгмïπдℓ ðѓáшş çύšτом ģļўφħѕ ƒǿя ъĺôсĸ éℓêm℮ήŧ àñð ъο× ðгдωΐπĝ čћдŗąςŧέѓś ìπŝţėãđ ŏƒ ũŝіⁿğ ťћę ƒбηŧ. Ţћιş ƒзаτџŕè ίš úπâνάίļǻвĺз ẅнέи μşιńĝ Ďΐґеčт2Ď άѕ тħë Ĝяãрнĩςŝ ÃРÌ. !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !!! !! A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw index 592a519ebd5..2ff2287c04e 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ru-RU/Resources.resw @@ -895,6 +895,9 @@ Начертание шрифта Name for a control to select the font for text in the app. + + Вы можете использовать несколько шрифтов, разделив их запятой ASCII. + Размер шрифта Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - Если этот параметр включен, терминал рисует собственные глифы для элементов блока и символов рисования прямоугольников вместо использования шрифта. Эта функция работает только тогда, когда доступно ускорение графического процессора. + Если этот параметр включен, терминал рисует собственные глифы для элементов блока и символов рисования прямоугольников вместо использования шрифта. Эта функция недоступна при использовании Direct2D в качестве API графики. A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw index 6461b722902..b70e55b6996 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/zh-CN/Resources.resw @@ -895,6 +895,9 @@ 字体 Name for a control to select the font for text in the app. + + 通过使用 ASCII 逗号分隔多个字体,可以使用这些字体。 + 字号 Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - 启用后,终端将为块元素和制表符绘制自定义标志符号,而不是使用字体。仅当 GPU 加速可用时,此功能才有效。 + 启用后,终端会使用自定义字形来绘制块元素和框线绘图字符,而不是使用字体。使用 Direct2D 作为图形 API 时,此功能不可用。 A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. diff --git a/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw index f951dbc2d9e..2501d9d7be4 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/zh-TW/Resources.resw @@ -895,6 +895,9 @@ 字體 Name for a control to select the font for text in the app. + + 您可以使用 ASCII 逗號分隔多個字型。 + 字型大小 Header for a control to determine the size of the text in the app. @@ -928,7 +931,7 @@ The main label of a toggle. When enabled, certain characters (glyphs) are replaced with better looking ones. - 啟用時,終端機會為區塊元素和方塊繪圖字元繪製自訂字符,而非使用字型。此功能僅在可使用 GPU 加速時作用。 + 啟用時,終端機會為區塊元素和方塊繪圖字元繪製自訂字符,而非使用字型。此功能無法在使用 Direct2D 做為圖形 API 時使用。 A longer description of the "Profile_EnableBuiltinGlyphs" toggle. "glyphs", "block element" and "box drawing characters" are technical terms from the Unicode specification. From 097a2c11362514ea4154576dd2afac14aac3152f Mon Sep 17 00:00:00 2001 From: "Dustin L. Howett" Date: Fri, 17 May 2024 15:46:52 -0500 Subject: [PATCH 25/39] Revert Canary to supporting 10.0.19041 (Windows 10 Vb+) (#17284) The 2024.04D servicing update to Windows 10 added support for `appLicensing`. --- src/cascadia/CascadiaPackage/Package-Can.appxmanifest | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest index 164ce0bca43..72b0a648789 100644 --- a/src/cascadia/CascadiaPackage/Package-Can.appxmanifest +++ b/src/cascadia/CascadiaPackage/Package-Can.appxmanifest @@ -38,9 +38,7 @@ - - - + From 4a243f044572146e18e0051badb1b5b3f3c28ac8 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Fri, 17 May 2024 22:49:23 +0100 Subject: [PATCH 26/39] Add support for VT paging operations (#16615) This PR adds support for multiples pages in the VT architecture, along with new operations for moving between those pages: `NP` (Next Page), `PP` (Preceding Page), `PPA` (Page Position Absolute), `PPR` (Page Position Relative), and `PPB` (Page Position Back). There's also a new mode, `DECPCCM` (Page Cursor Coupling Mode), which determines whether or not the active page is also the visible page, and a new query sequence, `DECRQDE` (Request Displayed Extent), which can be used to query the visible page. ## References and Relevant Issues When combined with `DECCRA` (Copy Rectangular Area), which can copy between pages, you can layer content on top of existing output, and still restore the original data afterwards. So this could serve as an alternative solution to #10810. ## Detailed Description of the Pull Request / Additional comments On the original DEC terminals that supported paging, you couldn't have both paging and scrollback at the same time - only the one or the other. But modern terminals typically allow both, so we support that too. The way it works, the currently visible page will be attached to the scrollback, and any content that scrolls off the top will thus be saved. But the background pages will not have scrollback, so their content is lost if it scrolls off the top. And when the screen is resized, only the visible page will be reflowed. Background pages are not affected by a resize until they become active. At that point they just receive the traditional style of resize, where the content is clipped or padded to match the new dimensions. I'm not sure this is the best way to handle resizing, but we can always consider other approaches once people have had a chance to try it out. ## Validation Steps Performed I've added some unit tests covering the new operations, and also done a lot of manual testing. Closes #13892 Tests added/passed --- .github/actions/spelling/expect/expect.txt | 4 + src/cascadia/TerminalCore/Terminal.hpp | 3 +- src/cascadia/TerminalCore/TerminalApi.cpp | 9 +- .../UnitTests_TerminalCore/SelectionTest.cpp | 23 +- .../TerminalApiTest.cpp | 3 +- src/host/outputStream.cpp | 21 +- src/host/outputStream.hpp | 3 +- src/terminal/adapter/DispatchTypes.hpp | 1 + src/terminal/adapter/ITermDispatch.hpp | 6 + src/terminal/adapter/ITerminalApi.hpp | 10 +- src/terminal/adapter/InteractDispatch.cpp | 4 +- src/terminal/adapter/PageManager.cpp | 254 +++++ src/terminal/adapter/PageManager.hpp | 67 ++ src/terminal/adapter/adaptDispatch.cpp | 890 ++++++++++-------- src/terminal/adapter/adaptDispatch.hpp | 32 +- .../adapter/adaptDispatchGraphics.cpp | 18 +- src/terminal/adapter/lib/adapter.vcxproj | 2 + .../adapter/lib/adapter.vcxproj.filters | 6 + src/terminal/adapter/sources.inc | 1 + src/terminal/adapter/termDispatch.hpp | 6 + .../adapter/ut_adapter/adapterTest.cpp | 147 ++- .../parser/OutputStateMachineEngine.cpp | 18 + .../parser/OutputStateMachineEngine.hpp | 6 + 23 files changed, 1059 insertions(+), 475 deletions(-) create mode 100644 src/terminal/adapter/PageManager.cpp create mode 100644 src/terminal/adapter/PageManager.hpp diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index 5c0c14b0df9..1a189277aef 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -406,6 +406,7 @@ DECNKM DECNRCM DECOM decommit +DECPCCM DECPCTERM DECPS DECRARA @@ -414,6 +415,7 @@ DECREQTPARM DECRLM DECRPM DECRQCRA +DECRQDE DECRQM DECRQPSR DECRQSS @@ -2123,6 +2125,7 @@ XIn XManifest XMath xorg +XPan XResource xsi xstyler @@ -2142,6 +2145,7 @@ YCast YCENTER YCount YLimit +YPan YSubstantial YVIRTUALSCREEN YWalk diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 75b822b1386..bf1120f421a 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -131,8 +131,7 @@ class Microsoft::Terminal::Core::Terminal final : // These methods are defined in TerminalApi.cpp void ReturnResponse(const std::wstring_view response) override; Microsoft::Console::VirtualTerminal::StateMachine& GetStateMachine() noexcept override; - TextBuffer& GetTextBuffer() noexcept override; - til::rect GetViewport() const noexcept override; + BufferState GetBufferAndViewport() noexcept override; void SetViewportPosition(const til::point position) noexcept override; void SetTextAttributes(const TextAttribute& attrs) noexcept override; void SetSystemMode(const Mode mode, const bool enabled) noexcept override; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 9b85b18d5ff..aa06c101295 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -34,14 +34,9 @@ Microsoft::Console::VirtualTerminal::StateMachine& Terminal::GetStateMachine() n return *_stateMachine; } -TextBuffer& Terminal::GetTextBuffer() noexcept +ITerminalApi::BufferState Terminal::GetBufferAndViewport() noexcept { - return _activeBuffer(); -} - -til::rect Terminal::GetViewport() const noexcept -{ - return til::rect{ _GetMutableViewport().ToInclusive() }; + return { _activeBuffer(), til::rect{ _GetMutableViewport().ToInclusive() }, !_inAltBuffer() }; } void Terminal::SetViewportPosition(const til::point position) noexcept diff --git a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp index 195eb17fcc3..1e55fb1f6e9 100644 --- a/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/SelectionTest.cpp @@ -44,6 +44,11 @@ namespace TerminalCoreUnitTests VERIFY_ARE_EQUAL(selection, expected); } + TextBuffer& GetTextBuffer(Terminal& term) + { + return term.GetBufferAndViewport().buffer; + } + TEST_METHOD(SelectUnit) { Terminal term{ Terminal::TestDummyMarker{} }; @@ -394,7 +399,7 @@ namespace TerminalCoreUnitTests const auto burrito = L"\xD83C\xDF2F"; // Insert wide glyph at position (4,10) - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(burrito); // Simulate click at (x,y) = (5,10) @@ -417,7 +422,7 @@ namespace TerminalCoreUnitTests const auto burrito = L"\xD83C\xDF2F"; // Insert wide glyph at position (4,10) - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(burrito); // Simulate click at (x,y) = (5,10) @@ -440,11 +445,11 @@ namespace TerminalCoreUnitTests const auto burrito = L"\xD83C\xDF2F"; // Insert wide glyph at position (4,10) - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(burrito); // Insert wide glyph at position (7,11) - term.GetTextBuffer().GetCursor().SetPosition({ 7, 11 }); + GetTextBuffer(term).GetCursor().SetPosition({ 7, 11 }); term.Write(burrito); // Simulate ALT + click at (x,y) = (5,8) @@ -496,7 +501,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"doubleClickMe"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate double click at (x,y) = (5,10) @@ -540,7 +545,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"C:\\Terminal>"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate click at (x,y) = (15,10) @@ -568,7 +573,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"doubleClickMe dragThroughHere"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate double click at (x,y) = (5,10) @@ -597,7 +602,7 @@ namespace TerminalCoreUnitTests // Insert text at position (21,10) const std::wstring_view text = L"doubleClickMe dragThroughHere"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Simulate double click at (x,y) = (21,10) @@ -685,7 +690,7 @@ namespace TerminalCoreUnitTests // Insert text at position (4,10) const std::wstring_view text = L"doubleClickMe dragThroughHere"; - term.GetTextBuffer().GetCursor().SetPosition({ 4, 10 }); + GetTextBuffer(term).GetCursor().SetPosition({ 4, 10 }); term.Write(text); // Step 1: Create a selection on "doubleClickMe" diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp index 9f002db5098..157bad1805d 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp @@ -152,7 +152,8 @@ void TerminalApiTest::CursorVisibility() VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn()); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); - term.GetTextBuffer().GetCursor().SetIsVisible(false); + auto& textBuffer = term.GetBufferAndViewport().buffer; + textBuffer.GetCursor().SetIsVisible(false); VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsVisible()); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn()); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); diff --git a/src/host/outputStream.cpp b/src/host/outputStream.cpp index fb20d74b75b..151a472f69d 100644 --- a/src/host/outputStream.cpp +++ b/src/host/outputStream.cpp @@ -52,25 +52,16 @@ StateMachine& ConhostInternalGetSet::GetStateMachine() } // Routine Description: -// - Retrieves the text buffer for the active output buffer. +// - Retrieves the text buffer and virtual viewport for the active output +// buffer. Also returns a flag indicating whether it's the main buffer. // Arguments: // - // Return Value: -// - a reference to the TextBuffer instance. -TextBuffer& ConhostInternalGetSet::GetTextBuffer() +// - a tuple with the buffer reference, viewport, and main buffer flag. +ITerminalApi::BufferState ConhostInternalGetSet::GetBufferAndViewport() { - return _io.GetActiveOutputBuffer().GetTextBuffer(); -} - -// Routine Description: -// - Retrieves the virtual viewport of the active output buffer. -// Arguments: -// - -// Return Value: -// - the exclusive coordinates of the viewport. -til::rect ConhostInternalGetSet::GetViewport() const -{ - return _io.GetActiveOutputBuffer().GetVirtualViewport().ToExclusive(); + auto& info = _io.GetActiveOutputBuffer(); + return { info.GetTextBuffer(), info.GetVirtualViewport().ToExclusive(), info.Next == nullptr }; } // Routine Description: diff --git a/src/host/outputStream.hpp b/src/host/outputStream.hpp index 01d8abaf17f..36283087ac2 100644 --- a/src/host/outputStream.hpp +++ b/src/host/outputStream.hpp @@ -32,8 +32,7 @@ class ConhostInternalGetSet final : public Microsoft::Console::VirtualTerminal:: void ReturnResponse(const std::wstring_view response) override; Microsoft::Console::VirtualTerminal::StateMachine& GetStateMachine() override; - TextBuffer& GetTextBuffer() override; - til::rect GetViewport() const override; + BufferState GetBufferAndViewport() override; void SetViewportPosition(const til::point position) override; void SetTextAttributes(const TextAttribute& attrs) override; diff --git a/src/terminal/adapter/DispatchTypes.hpp b/src/terminal/adapter/DispatchTypes.hpp index 52852bed6a2..a0a74d8c83a 100644 --- a/src/terminal/adapter/DispatchTypes.hpp +++ b/src/terminal/adapter/DispatchTypes.hpp @@ -531,6 +531,7 @@ namespace Microsoft::Console::VirtualTerminal::DispatchTypes ATT610_StartCursorBlink = DECPrivateMode(12), DECTCEM_TextCursorEnableMode = DECPrivateMode(25), XTERM_EnableDECCOLMSupport = DECPrivateMode(40), + DECPCCM_PageCursorCouplingMode = DECPrivateMode(64), DECNKM_NumericKeypadMode = DECPrivateMode(66), DECBKM_BackarrowKeyMode = DECPrivateMode(67), DECLRMM_LeftRightMarginMode = DECPrivateMode(69), diff --git a/src/terminal/adapter/ITermDispatch.hpp b/src/terminal/adapter/ITermDispatch.hpp index eed892f5a94..e3976996433 100644 --- a/src/terminal/adapter/ITermDispatch.hpp +++ b/src/terminal/adapter/ITermDispatch.hpp @@ -49,6 +49,12 @@ class Microsoft::Console::VirtualTerminal::ITermDispatch virtual bool DeleteCharacter(const VTInt count) = 0; // DCH virtual bool ScrollUp(const VTInt distance) = 0; // SU virtual bool ScrollDown(const VTInt distance) = 0; // SD + virtual bool NextPage(const VTInt pageCount) = 0; // NP + virtual bool PrecedingPage(const VTInt pageCount) = 0; // PP + virtual bool PagePositionAbsolute(const VTInt page) = 0; // PPA + virtual bool PagePositionRelative(const VTInt pageCount) = 0; // PPR + virtual bool PagePositionBack(const VTInt pageCount) = 0; // PPB + virtual bool RequestDisplayedExtent() = 0; // DECRQDE virtual bool InsertLine(const VTInt distance) = 0; // IL virtual bool DeleteLine(const VTInt distance) = 0; // DL virtual bool InsertColumn(const VTInt distance) = 0; // DECIC diff --git a/src/terminal/adapter/ITerminalApi.hpp b/src/terminal/adapter/ITerminalApi.hpp index 4381c4ecbdd..2c3aec044ed 100644 --- a/src/terminal/adapter/ITerminalApi.hpp +++ b/src/terminal/adapter/ITerminalApi.hpp @@ -39,9 +39,15 @@ namespace Microsoft::Console::VirtualTerminal virtual void ReturnResponse(const std::wstring_view response) = 0; + struct BufferState + { + TextBuffer& buffer; + til::rect viewport; + bool isMainBuffer; + }; + virtual StateMachine& GetStateMachine() = 0; - virtual TextBuffer& GetTextBuffer() = 0; - virtual til::rect GetViewport() const = 0; + virtual BufferState GetBufferAndViewport() = 0; virtual void SetViewportPosition(const til::point position) = 0; virtual bool IsVtInputEnabled() const = 0; diff --git a/src/terminal/adapter/InteractDispatch.cpp b/src/terminal/adapter/InteractDispatch.cpp index cb06c985c91..a102368ec5b 100644 --- a/src/terminal/adapter/InteractDispatch.cpp +++ b/src/terminal/adapter/InteractDispatch.cpp @@ -108,7 +108,7 @@ bool InteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulatio _api.ShowWindow(false); return true; case DispatchTypes::WindowManipulationType::RefreshWindow: - _api.GetTextBuffer().TriggerRedrawAll(); + _api.GetBufferAndViewport().buffer.TriggerRedrawAll(); return true; case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: // TODO:GH#1765 We should introduce a better `ResizeConpty` function to @@ -135,7 +135,7 @@ bool InteractDispatch::WindowManipulation(const DispatchTypes::WindowManipulatio bool InteractDispatch::MoveCursor(const VTInt row, const VTInt col) { // First retrieve some information about the buffer - const auto viewport = _api.GetViewport(); + const auto viewport = _api.GetBufferAndViewport().viewport; // In VT, the origin is 1,1. For our array, it's 0,0. So subtract 1. // Apply boundary tests to ensure the cursor isn't outside the viewport rectangle. diff --git a/src/terminal/adapter/PageManager.cpp b/src/terminal/adapter/PageManager.cpp new file mode 100644 index 00000000000..5408b249738 --- /dev/null +++ b/src/terminal/adapter/PageManager.cpp @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +#include "precomp.h" + +#include "PageManager.hpp" +#include "../../renderer/base/renderer.hpp" + +using namespace Microsoft::Console::VirtualTerminal; + +Page::Page(TextBuffer& buffer, const til::rect& viewport, const til::CoordType number) noexcept : + _buffer{ buffer }, + _viewport{ viewport }, + _number(number) +{ +} + +TextBuffer& Page::Buffer() const noexcept +{ + return _buffer; +} + +til::rect Page::Viewport() const noexcept +{ + return _viewport; +} + +til::CoordType Page::Number() const noexcept +{ + return _number; +} + +Cursor& Page::Cursor() const noexcept +{ + return _buffer.GetCursor(); +} + +const TextAttribute& Page::Attributes() const noexcept +{ + return _buffer.GetCurrentAttributes(); +} + +void Page::SetAttributes(const TextAttribute& attr, ITerminalApi* api) const +{ + _buffer.SetCurrentAttributes(attr); + // If the api parameter was specified, we need to pass the new attributes + // through to the api. This occurs when there's a potential for the colors + // to be changed, which may require some legacy remapping in conhost. + if (api) + { + api->SetTextAttributes(attr); + } +} + +til::CoordType Page::Top() const noexcept +{ + // If we ever support vertical window panning, the page top won't + // necessarily align with the viewport top, so it's best we always + // treat them as distinct properties. + return _viewport.top; +} + +til::CoordType Page::Bottom() const noexcept +{ + // Similarly, the page bottom won't always match the viewport bottom. + return _viewport.bottom; +} + +til::CoordType Page::Width() const noexcept +{ + // The page width could also one day be different from the buffer width, + // so again it's best treated as a distinct property. + return _buffer.GetSize().Width(); +} + +til::CoordType Page::Height() const noexcept +{ + return Bottom() - Top(); +} + +til::CoordType Page::BufferHeight() const noexcept +{ + return _buffer.GetSize().Height(); +} + +til::CoordType Page::XPanOffset() const noexcept +{ + return _viewport.left; +} + +til::CoordType Page::YPanOffset() const noexcept +{ + return 0; // Vertical panning is not yet supported +} + +PageManager::PageManager(ITerminalApi& api, Renderer& renderer) noexcept : + _api{ api }, + _renderer{ renderer } +{ +} + +void PageManager::Reset() +{ + _activePageNumber = 1; + _visiblePageNumber = 1; + _buffers = {}; +} + +Page PageManager::Get(const til::CoordType pageNumber) const +{ + const auto requestedPageNumber = std::min(std::max(pageNumber, 1), MAX_PAGES); + auto [visibleBuffer, visibleViewport, isMainBuffer] = _api.GetBufferAndViewport(); + + // If we're not in the main buffer (either because an app has enabled the + // alternate buffer mode, or switched the conhost screen buffer), then VT + // paging doesn't apply, so we disregard the requested page number and just + // use the visible buffer (with a fixed page number of 1). + if (!isMainBuffer) + { + return { visibleBuffer, visibleViewport, 1 }; + } + + // If the requested page number happens to be the visible page, then we + // can also just use the visible buffer as is. + if (requestedPageNumber == _visiblePageNumber) + { + return { visibleBuffer, visibleViewport, _visiblePageNumber }; + } + + // Otherwise we're working with a background buffer, so we need to + // retrieve that from the buffer array, and resize it to match the + // active page size. + const auto pageSize = visibleViewport.size(); + auto& pageBuffer = _getBuffer(requestedPageNumber, pageSize); + return { pageBuffer, til::rect{ pageSize }, requestedPageNumber }; +} + +Page PageManager::ActivePage() const +{ + return Get(_activePageNumber); +} + +Page PageManager::VisiblePage() const +{ + return Get(_visiblePageNumber); +} + +void PageManager::MoveTo(const til::CoordType pageNumber, const bool makeVisible) +{ + auto [visibleBuffer, visibleViewport, isMainBuffer] = _api.GetBufferAndViewport(); + if (!isMainBuffer) + { + return; + } + + const auto pageSize = visibleViewport.size(); + const auto visibleTop = visibleViewport.top; + const auto wasVisible = _activePageNumber == _visiblePageNumber; + const auto newPageNumber = std::min(std::max(pageNumber, 1), MAX_PAGES); + auto redrawRequired = false; + + // If we're changing the visible page, what we do is swap out the current + // visible page into its backing buffer, and swap in the new page from the + // backing buffer to the main buffer. That way the rest of the system only + // ever has to deal with the main buffer. + if (makeVisible && _visiblePageNumber != newPageNumber) + { + const auto& newBuffer = _getBuffer(newPageNumber, pageSize); + auto& saveBuffer = _getBuffer(_visiblePageNumber, pageSize); + for (auto i = 0; i < pageSize.height; i++) + { + saveBuffer.GetMutableRowByOffset(i).CopyFrom(visibleBuffer.GetRowByOffset(visibleTop + i)); + } + for (auto i = 0; i < pageSize.height; i++) + { + visibleBuffer.GetMutableRowByOffset(visibleTop + i).CopyFrom(newBuffer.GetRowByOffset(i)); + } + _visiblePageNumber = newPageNumber; + redrawRequired = true; + } + + // If the active page was previously visible, and is now still visible, + // there is no need to update any buffer properties, because we'll have + // been using the main buffer in both cases. + const auto isVisible = newPageNumber == _visiblePageNumber; + if (!wasVisible || !isVisible) + { + // Otherwise we need to copy the properties from the old buffer to the + // new, so we retain the current attributes and cursor position. This + // is only needed if they are actually different. + auto& oldBuffer = wasVisible ? visibleBuffer : _getBuffer(_activePageNumber, pageSize); + auto& newBuffer = isVisible ? visibleBuffer : _getBuffer(newPageNumber, pageSize); + if (&oldBuffer != &newBuffer) + { + // When copying the cursor position, we need to adjust the y + // coordinate to account for scrollback in the visible buffer. + const auto oldTop = wasVisible ? visibleTop : 0; + const auto newTop = isVisible ? visibleTop : 0; + auto position = oldBuffer.GetCursor().GetPosition(); + position.y = position.y - oldTop + newTop; + newBuffer.SetCurrentAttributes(oldBuffer.GetCurrentAttributes()); + newBuffer.CopyProperties(oldBuffer); + newBuffer.GetCursor().SetPosition(position); + } + // If we moved from the visible buffer to a background buffer we need + // to hide the cursor in the visible buffer. This is because the page + // number is like a third dimension in the cursor coordinate system. + // If the cursor isn't on the visible page, it's the same as if its + // x/y coordinates are outside the visible viewport. + if (wasVisible && !isVisible) + { + visibleBuffer.GetCursor().SetIsVisible(false); + } + } + + _activePageNumber = newPageNumber; + if (redrawRequired) + { + _renderer.TriggerRedrawAll(); + } +} + +void PageManager::MoveRelative(const til::CoordType pageCount, const bool makeVisible) +{ + MoveTo(_activePageNumber + pageCount, makeVisible); +} + +void PageManager::MakeActivePageVisible() +{ + if (_activePageNumber != _visiblePageNumber) + { + MoveTo(_activePageNumber, true); + } +} + +TextBuffer& PageManager::_getBuffer(const til::CoordType pageNumber, const til::size pageSize) const +{ + auto& buffer = til::at(_buffers, pageNumber - 1); + if (buffer == nullptr) + { + // Page buffers are created on demand, and are sized to match the active + // page dimensions without any scrollback rows. + buffer = std::make_unique(pageSize, TextAttribute{}, 0, false, _renderer); + } + else if (buffer->GetSize().Dimensions() != pageSize) + { + // If a buffer already exists for the page, and the page dimensions have + // changed while it was inactive, it will need to be resized. + // TODO: We don't currently reflow the existing content in this case, but + // that may be something we want to reconsider. + buffer->ResizeTraditional(pageSize); + } + return *buffer; +} diff --git a/src/terminal/adapter/PageManager.hpp b/src/terminal/adapter/PageManager.hpp new file mode 100644 index 00000000000..ab913902609 --- /dev/null +++ b/src/terminal/adapter/PageManager.hpp @@ -0,0 +1,67 @@ +/*++ +Copyright (c) Microsoft Corporation +Licensed under the MIT license. + +Module Name: +- PageManager.hpp + +Abstract: +- This manages the text buffers required by the VT paging operations. +--*/ + +#pragma once + +#include "ITerminalApi.hpp" +#include "til.h" + +namespace Microsoft::Console::VirtualTerminal +{ + class Page + { + public: + Page(TextBuffer& buffer, const til::rect& viewport, const til::CoordType number) noexcept; + TextBuffer& Buffer() const noexcept; + til::rect Viewport() const noexcept; + til::CoordType Number() const noexcept; + Cursor& Cursor() const noexcept; + const TextAttribute& Attributes() const noexcept; + void SetAttributes(const TextAttribute& attr, ITerminalApi* api = nullptr) const; + til::CoordType Top() const noexcept; + til::CoordType Bottom() const noexcept; + til::CoordType Width() const noexcept; + til::CoordType Height() const noexcept; + til::CoordType BufferHeight() const noexcept; + til::CoordType XPanOffset() const noexcept; + til::CoordType YPanOffset() const noexcept; + + private: + TextBuffer& _buffer; + til::rect _viewport; + til::CoordType _number; + }; + + class PageManager + { + using Renderer = Microsoft::Console::Render::Renderer; + + public: + PageManager(ITerminalApi& api, Renderer& renderer) noexcept; + void Reset(); + Page Get(const til::CoordType pageNumber) const; + Page ActivePage() const; + Page VisiblePage() const; + void MoveTo(const til::CoordType pageNumber, const bool makeVisible); + void MoveRelative(const til::CoordType pageCount, const bool makeVisible); + void MakeActivePageVisible(); + + private: + TextBuffer& _getBuffer(const til::CoordType pageNumber, const til::size pageSize) const; + + ITerminalApi& _api; + Renderer& _renderer; + til::CoordType _activePageNumber = 1; + til::CoordType _visiblePageNumber = 1; + static constexpr til::CoordType MAX_PAGES = 6; + mutable std::array, MAX_PAGES> _buffers; + }; +} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 02c22db8eec..4135cac88d6 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -22,7 +22,8 @@ AdaptDispatch::AdaptDispatch(ITerminalApi& api, Renderer& renderer, RenderSettin _renderSettings{ renderSettings }, _terminalInput{ terminalInput }, _usingAltBuffer(false), - _termOutput() + _termOutput(), + _pages{ api, renderer } { } @@ -72,15 +73,15 @@ void AdaptDispatch::PrintString(const std::wstring_view string) void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) { - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& textBuffer = page.Buffer(); + auto& cursor = page.Cursor(); auto cursorPosition = cursor.GetPosition(); const auto wrapAtEOL = _api.GetSystemMode(ITerminalApi::Mode::AutoWrap); - const auto& attributes = textBuffer.GetCurrentAttributes(); + const auto& attributes = page.Attributes(); - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(textBuffer.GetSize().Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); auto lineWidth = textBuffer.GetLineWidth(cursorPosition.y); if (cursorPosition.x <= rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) @@ -106,7 +107,7 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) // different position from where the EOL was marked. if (delayedCursorPosition == cursorPosition) { - _DoLineFeed(textBuffer, true, true); + _DoLineFeed(page, true, true); cursorPosition = cursor.GetPosition(); // We need to recalculate the width when moving to a new line. lineWidth = textBuffer.GetLineWidth(cursorPosition.y); @@ -266,19 +267,19 @@ bool AdaptDispatch::CursorPrevLine(const VTInt distance) // Routine Description: // - Returns the coordinates of the vertical scroll margins. // Arguments: -// - viewport - The viewport rect (exclusive). -// - absolute - Should coordinates be absolute or relative to the viewport. +// - page - The page that the margins will apply to. +// - absolute - Should coordinates be absolute or relative to the page top. // Return Value: // - A std::pair containing the top and bottom coordinates (inclusive). -std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept +std::pair AdaptDispatch::_GetVerticalMargins(const Page& page, const bool absolute) noexcept { // If the top is out of range, reset the margins completely. - const auto bottommostRow = viewport.bottom - viewport.top - 1; + const auto bottommostRow = page.Height() - 1; if (_scrollMargins.top >= bottommostRow) { _scrollMargins.top = _scrollMargins.bottom = 0; } - // If margins aren't set, use the full extent of the viewport. + // If margins aren't set, use the full extent of the page. const auto marginsSet = _scrollMargins.top < _scrollMargins.bottom; auto topMargin = marginsSet ? _scrollMargins.top : 0; auto bottomMargin = marginsSet ? _scrollMargins.bottom : bottommostRow; @@ -286,8 +287,8 @@ std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport bottomMargin = std::min(bottomMargin, bottommostRow); if (absolute) { - topMargin += viewport.top; - bottomMargin += viewport.top; + topMargin += page.Top(); + bottomMargin += page.Top(); } return { topMargin, bottomMargin }; } @@ -295,13 +296,13 @@ std::pair AdaptDispatch::_GetVerticalMargins(const til::rect& viewport // Routine Description: // - Returns the coordinates of the horizontal scroll margins. // Arguments: -// - bufferWidth - The width of the buffer +// - pageWidth - The width of the page // Return Value: // - A std::pair containing the left and right coordinates (inclusive). -std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType bufferWidth) noexcept +std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType pageWidth) noexcept { // If the left is out of range, reset the margins completely. - const auto rightmostColumn = bufferWidth - 1; + const auto rightmostColumn = pageWidth - 1; if (_scrollMargins.left >= rightmostColumn) { _scrollMargins.left = _scrollMargins.right = 0; @@ -326,13 +327,12 @@ std::pair AdaptDispatch::_GetHorizontalMargins(const til::CoordType bu bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins) { // First retrieve some information about the buffer - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); - const auto bufferWidth = textBuffer.GetSize().Width(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); + const auto pageWidth = page.Width(); const auto cursorPosition = cursor.GetPosition(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth); // For relative movement, the given offsets will be relative to // the current cursor position. @@ -340,10 +340,10 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col auto col = cursorPosition.x; // But if the row is absolute, it will be relative to the top of the - // viewport, or the top margin, depending on the origin mode. + // page, or the top margin, depending on the origin mode. if (rowOffset.IsAbsolute) { - row = _modes.test(Mode::Origin) ? topMargin : viewport.top; + row = _modes.test(Mode::Origin) ? topMargin : page.Top(); } // And if the column is absolute, it'll be relative to column 0, @@ -355,10 +355,10 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col } // Adjust the base position by the given offsets and clamp the results. - // The row is constrained within the viewport's vertical boundaries, + // The row is constrained within the page's vertical boundaries, // while the column is constrained by the buffer width. - row = std::clamp(row + rowOffset.Value, viewport.top, viewport.bottom - 1); - col = std::clamp(col + colOffset.Value, 0, bufferWidth - 1); + row = std::clamp(row + rowOffset.Value, page.Top(), page.Bottom() - 1); + col = std::clamp(col + colOffset.Value, 0, pageWidth - 1); // If the operation needs to be clamped inside the margins, or the origin // mode is relative (which always requires margin clamping), then the row @@ -398,7 +398,7 @@ bool AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col } // Finally, attempt to set the adjusted cursor position back into the console. - cursor.SetPosition(textBuffer.ClampPositionWithinLine({ col, row })); + cursor.SetPosition(page.Buffer().ClampPositionWithinLine({ col, row })); _ApplyCursorMovementFlags(cursor); return true; @@ -490,29 +490,28 @@ bool AdaptDispatch::CursorPosition(const VTInt line, const VTInt column) bool AdaptDispatch::CursorSaveState() { // First retrieve some information about the buffer - const auto viewport = _api.GetViewport(); - const auto& textBuffer = _api.GetTextBuffer(); - const auto& attributes = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); // The cursor is given to us by the API as relative to the whole buffer. - // But in VT speak, the cursor row should be relative to the current viewport top. - auto cursorPosition = textBuffer.GetCursor().GetPosition(); - cursorPosition.y -= viewport.top; + // But in VT speak, the cursor row should be relative to the current page top. + auto cursorPosition = page.Cursor().GetPosition(); + cursorPosition.y -= page.Top(); // Although if origin mode is set, the cursor is relative to the margin origin. if (_modes.test(Mode::Origin)) { - cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; - cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + cursorPosition.x -= _GetHorizontalMargins(page.Width()).first; + cursorPosition.y -= _GetVerticalMargins(page, false).first; } // VT is also 1 based, not 0 based, so correct by 1. auto& savedCursorState = _savedCursorState.at(_usingAltBuffer); savedCursorState.Column = cursorPosition.x + 1; savedCursorState.Row = cursorPosition.y + 1; - savedCursorState.IsDelayedEOLWrap = textBuffer.GetCursor().IsDelayedEOLWrap(); + savedCursorState.Page = page.Number(); + savedCursorState.IsDelayedEOLWrap = page.Cursor().IsDelayedEOLWrap(); savedCursorState.IsOriginModeRelative = _modes.test(Mode::Origin); - savedCursorState.Attributes = attributes; + savedCursorState.Attributes = page.Attributes(); savedCursorState.TermOutput = _termOutput; return true; @@ -533,17 +532,21 @@ bool AdaptDispatch::CursorRestoreState() // Restore the origin mode first, since the cursor coordinates may be relative. _modes.set(Mode::Origin, savedCursorState.IsOriginModeRelative); + // Restore the page number. + PagePositionAbsolute(savedCursorState.Page); + // We can then restore the position with a standard CUP operation. CursorPosition(savedCursorState.Row, savedCursorState.Column); // If the delayed wrap flag was set when the cursor was saved, we need to restore that now. + const auto page = _pages.ActivePage(); if (savedCursorState.IsDelayedEOLWrap) { - _api.GetTextBuffer().GetCursor().DelayEOLWrap(); + page.Cursor().DelayEOLWrap(); } // Restore text attributes. - _api.SetTextAttributes(savedCursorState.Attributes); + page.SetAttributes(savedCursorState.Attributes, &_api); // Restore designated character sets. _termOutput.RestoreFrom(savedCursorState.TermOutput); @@ -556,10 +559,10 @@ bool AdaptDispatch::CursorRestoreState() // the Erase Color mode is set, we use the default attributes, but when reset, // we use the active color attributes with the character attributes cleared. // Arguments: -// - textBuffer - Target buffer that is being erased. +// - page - Target page that is being erased. // Return Value: // - The erase TextAttribute value. -TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) const noexcept +TextAttribute AdaptDispatch::_GetEraseAttributes(const Page& page) const noexcept { if (_modes.test(Mode::EraseColor)) { @@ -567,7 +570,7 @@ TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) c } else { - auto eraseAttributes = textBuffer.GetCurrentAttributes(); + auto eraseAttributes = page.Attributes(); eraseAttributes.SetStandardErase(); return eraseAttributes; } @@ -576,13 +579,14 @@ TextAttribute AdaptDispatch::_GetEraseAttributes(const TextBuffer& textBuffer) c // Routine Description: // - Scrolls an area of the buffer in a vertical direction. // Arguments: -// - textBuffer - Target buffer to be scrolled. -// - fillRect - Area of the buffer that will be affected. +// - page - Target page to be scrolled. +// - fillRect - Area of the page that will be affected. // - delta - Distance to move (positive is down, negative is up). // Return Value: // - -void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta) +void AdaptDispatch::_ScrollRectVertically(const Page& page, const til::rect& scrollRect, const VTInt delta) { + auto& textBuffer = page.Buffer(); const auto absoluteDelta = std::min(std::abs(delta), scrollRect.height()); if (absoluteDelta < scrollRect.height()) { @@ -590,7 +594,7 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec const auto width = scrollRect.width(); const auto height = scrollRect.height() - absoluteDelta; const auto actualDelta = delta > 0 ? absoluteDelta : -absoluteDelta; - if (width == textBuffer.GetSize().Width()) + if (width == page.Width()) { // If the scrollRect is the full width of the buffer, we can scroll // more efficiently by rotating the row storage. @@ -621,8 +625,8 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec auto eraseRect = scrollRect; eraseRect.top = delta > 0 ? scrollRect.top : (scrollRect.bottom - absoluteDelta); eraseRect.bottom = eraseRect.top + absoluteDelta; - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, eraseRect, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. textBuffer.ResetLineRenditionRange(eraseRect.top, eraseRect.bottom); @@ -631,13 +635,14 @@ void AdaptDispatch::_ScrollRectVertically(TextBuffer& textBuffer, const til::rec // Routine Description: // - Scrolls an area of the buffer in a horizontal direction. // Arguments: -// - textBuffer - Target buffer to be scrolled. -// - fillRect - Area of the buffer that will be affected. +// - page - Target page to be scrolled. +// - fillRect - Area of the page that will be affected. // - delta - Distance to move (positive is right, negative is left). // Return Value: // - -void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta) +void AdaptDispatch::_ScrollRectHorizontally(const Page& page, const til::rect& scrollRect, const VTInt delta) { + auto& textBuffer = page.Buffer(); const auto absoluteDelta = std::min(std::abs(delta), scrollRect.width()); if (absoluteDelta < scrollRect.width()) { @@ -669,8 +674,8 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r auto eraseRect = scrollRect; eraseRect.left = delta > 0 ? scrollRect.left : (scrollRect.right - absoluteDelta); eraseRect.right = eraseRect.left + absoluteDelta; - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, eraseRect, whitespace, eraseAttributes); } // Routine Description: @@ -682,20 +687,19 @@ void AdaptDispatch::_ScrollRectHorizontally(TextBuffer& textBuffer, const til::r // - void AdaptDispatch::_InsertDeleteCharacterHelper(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; - const auto lineWidth = textBuffer.GetLineWidth(row); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto page = _pages.ActivePage(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; + const auto lineWidth = page.Buffer().GetLineWidth(row); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); const auto [leftMargin, rightMargin] = (row >= topMargin && row <= bottomMargin) ? _GetHorizontalMargins(lineWidth) : std::make_pair(0, lineWidth - 1); if (col >= leftMargin && col <= rightMargin) { - _ScrollRectHorizontally(textBuffer, { col, row, rightMargin + 1, row + 1 }, delta); + _ScrollRectHorizontally(page, { col, row, rightMargin + 1, row + 1 }, delta); // The ICH and DCH controls are expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); } } @@ -728,15 +732,15 @@ bool AdaptDispatch::DeleteCharacter(const VTInt count) // Routine Description: // - Fills an area of the buffer with a given character and attributes. // Arguments: -// - textBuffer - Target buffer to be filled. -// - fillRect - Area of the buffer that will be affected. +// - page - Target page to be filled. +// - fillRect - Area of the page that will be affected. // - fillChar - Character to be written to the buffer. // - fillAttrs - Attributes to be written to the buffer. // Return Value: // - -void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const +void AdaptDispatch::_FillRect(const Page& page, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const { - textBuffer.FillRect(fillRect, fillChar, fillAttrs); + page.Buffer().FillRect(fillRect, fillChar, fillAttrs); _api.NotifyAccessibilityChange(fillRect); } @@ -751,28 +755,28 @@ void AdaptDispatch::_FillRect(TextBuffer& textBuffer, const til::rect& fillRect, // - True. bool AdaptDispatch::EraseCharacters(const VTInt numChars) { - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto startCol = textBuffer.GetCursor().GetPosition().x; - const auto endCol = std::min(startCol + numChars, textBuffer.GetLineWidth(row)); + const auto page = _pages.ActivePage(); + const auto row = page.Cursor().GetPosition().y; + const auto startCol = page.Cursor().GetPosition().x; + const auto endCol = std::min(startCol + numChars, page.Buffer().GetLineWidth(row)); // The ECH control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, { startCol, row, endCol, row + 1 }, whitespace, eraseAttributes); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, { startCol, row, endCol, row + 1 }, whitespace, eraseAttributes); return true; } // Routine Description: -// - ED - Erases a portion of the current viewable area (viewport) of the console. +// - ED - Erases a portion of the current page of the console. // Arguments: // - eraseType - Determines whether to erase: // From beginning (top-left corner) to the cursor // From cursor to end (bottom-right corner) -// The entire viewport area -// The scrollback (outside the viewport area) +// The entire page +// The scrollback (outside the page area) // Return Value: // - True if handled successfully. False otherwise. bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) @@ -783,7 +787,7 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // Scrollback clears erase everything in the "scrollback" of a *nix terminal // Everything that's scrolled off the screen so far. // Or if it's an Erase All, then we also need to handle that specially - // by moving the current contents of the viewport into the scrollback. + // by moving the current contents of the page into the scrollback. if (eraseType == DispatchTypes::EraseType::Scrollback) { return _EraseScrollback(); @@ -793,18 +797,18 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) return _EraseAll(); } - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + auto& textBuffer = page.Buffer(); + const auto pageWidth = page.Width(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The ED control is expected to reset the delayed wrap flag. // The special case variants above ("erase all" and "erase scrollback") // take care of that themselves when they set the cursor position. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); // When erasing the display, every line that is erased in full should be // reset to single width. When erasing to the end, this could include @@ -814,15 +818,15 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // the line is double width). if (eraseType == DispatchTypes::EraseType::FromBeginning) { - textBuffer.ResetLineRenditionRange(viewport.top, row); - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, row }, whitespace, eraseAttributes); - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); + textBuffer.ResetLineRenditionRange(page.Top(), row); + _FillRect(page, { 0, page.Top(), pageWidth, row }, whitespace, eraseAttributes); + _FillRect(page, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); } if (eraseType == DispatchTypes::EraseType::ToEnd) { - textBuffer.ResetLineRenditionRange(col > 0 ? row + 1 : row, viewport.bottom); - _FillRect(textBuffer, { col, row, bufferWidth, row + 1 }, whitespace, eraseAttributes); - _FillRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }, whitespace, eraseAttributes); + textBuffer.ResetLineRenditionRange(col > 0 ? row + 1 : row, page.Bottom()); + _FillRect(page, { col, row, pageWidth, row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { 0, row + 1, pageWidth, page.Bottom() }, whitespace, eraseAttributes); } return true; @@ -836,24 +840,25 @@ bool AdaptDispatch::EraseInDisplay(const DispatchTypes::EraseType eraseType) // - True if handled successfully. False otherwise. bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) { - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto& textBuffer = page.Buffer(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The EL control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _FillRect(textBuffer, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { 0, row, col + 1, row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::ToEnd: - _FillRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { col, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; case DispatchTypes::EraseType::All: - _FillRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); + _FillRect(page, { 0, row, textBuffer.GetLineWidth(row), row + 1 }, whitespace, eraseAttributes); return true; default: return false; @@ -863,17 +868,17 @@ bool AdaptDispatch::EraseInLine(const DispatchTypes::EraseType eraseType) // Routine Description: // - Selectively erases unprotected cells in an area of the buffer. // Arguments: -// - textBuffer - Target buffer to be erased. -// - eraseRect - Area of the buffer that will be affected. +// - page - Target page to be erased. +// - eraseRect - Area of the page that will be affected. // Return Value: // - -void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect) +void AdaptDispatch::_SelectiveEraseRect(const Page& page, const til::rect& eraseRect) { if (eraseRect) { for (auto row = eraseRect.top; row < eraseRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); + auto& rowBuffer = page.Buffer().GetMutableRowByOffset(row); for (auto col = eraseRect.left; col < eraseRect.right; col++) { // Only unprotected cells are affected. @@ -881,7 +886,7 @@ void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& { // The text is cleared but the attributes are left as is. rowBuffer.ClearCell(col); - textBuffer.TriggerRedraw(Viewport::FromCoord({ col, row })); + page.Buffer().TriggerRedraw(Viewport::FromCoord({ col, row })); } } } @@ -890,37 +895,36 @@ void AdaptDispatch::_SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& } // Routine Description: -// - DECSED - Selectively erases unprotected cells in a portion of the viewport. +// - DECSED - Selectively erases unprotected cells in a portion of the page. // Arguments: // - eraseType - Determines whether to erase: // From beginning (top-left corner) to the cursor // From cursor to end (bottom-right corner) -// The entire viewport area +// The entire page area // Return Value: // - True if handled successfully. False otherwise. bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType eraseType) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto pageWidth = page.Width(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The DECSED control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _SelectiveEraseRect(textBuffer, { 0, viewport.top, bufferWidth, row }); - _SelectiveEraseRect(textBuffer, { 0, row, col + 1, row + 1 }); + _SelectiveEraseRect(page, { 0, page.Top(), pageWidth, row }); + _SelectiveEraseRect(page, { 0, row, col + 1, row + 1 }); return true; case DispatchTypes::EraseType::ToEnd: - _SelectiveEraseRect(textBuffer, { col, row, bufferWidth, row + 1 }); - _SelectiveEraseRect(textBuffer, { 0, row + 1, bufferWidth, viewport.bottom }); + _SelectiveEraseRect(page, { col, row, pageWidth, row + 1 }); + _SelectiveEraseRect(page, { 0, row + 1, pageWidth, page.Bottom() }); return true; case DispatchTypes::EraseType::All: - _SelectiveEraseRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }); + _SelectiveEraseRect(page, { 0, page.Top(), pageWidth, page.Bottom() }); return true; default: return false; @@ -938,23 +942,24 @@ bool AdaptDispatch::SelectiveEraseInDisplay(const DispatchTypes::EraseType erase // - True if handled successfully. False otherwise. bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseType) { - auto& textBuffer = _api.GetTextBuffer(); - const auto row = textBuffer.GetCursor().GetPosition().y; - const auto col = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto& textBuffer = page.Buffer(); + const auto row = page.Cursor().GetPosition().y; + const auto col = page.Cursor().GetPosition().x; // The DECSEL control is expected to reset the delayed wrap flag. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); switch (eraseType) { case DispatchTypes::EraseType::FromBeginning: - _SelectiveEraseRect(textBuffer, { 0, row, col + 1, row + 1 }); + _SelectiveEraseRect(page, { 0, row, col + 1, row + 1 }); return true; case DispatchTypes::EraseType::ToEnd: - _SelectiveEraseRect(textBuffer, { col, row, textBuffer.GetLineWidth(row), row + 1 }); + _SelectiveEraseRect(page, { col, row, textBuffer.GetLineWidth(row), row + 1 }); return true; case DispatchTypes::EraseType::All: - _SelectiveEraseRect(textBuffer, { 0, row, textBuffer.GetLineWidth(row), row + 1 }); + _SelectiveEraseRect(page, { 0, row, textBuffer.GetLineWidth(row), row + 1 }); return true; default: return false; @@ -964,18 +969,18 @@ bool AdaptDispatch::SelectiveEraseInLine(const DispatchTypes::EraseType eraseTyp // Routine Description: // - Changes the attributes of each cell in a rectangular area of the buffer. // Arguments: -// - textBuffer - Target buffer to be changed. -// - changeRect - A rectangular area of the buffer that will be affected. +// - page - Target page to be changed. +// - changeRect - A rectangular area of the page that will be affected. // - changeOps - Changes that will be applied to each of the attributes. // Return Value: // - -void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps) +void AdaptDispatch::_ChangeRectAttributes(const Page& page, const til::rect& changeRect, const ChangeOps& changeOps) { if (changeRect) { for (auto row = changeRect.top; row < changeRect.bottom; row++) { - auto& rowBuffer = textBuffer.GetMutableRowByOffset(row); + auto& rowBuffer = page.Buffer().GetMutableRowByOffset(row); for (auto col = changeRect.left; col < changeRect.right; col++) { auto attr = rowBuffer.GetAttrByColumn(col); @@ -998,7 +1003,7 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec rowBuffer.ReplaceAttributes(col, col + 1, attr); } } - textBuffer.TriggerRedraw(Viewport::FromExclusive(changeRect)); + page.Buffer().TriggerRedraw(Viewport::FromExclusive(changeRect)); _api.NotifyAccessibilityChange(changeRect); } } @@ -1014,16 +1019,15 @@ void AdaptDispatch::_ChangeRectAttributes(TextBuffer& textBuffer, const til::rec // - void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps) { - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize().Dimensions(); - const auto changeRect = _CalculateRectArea(changeArea.top, changeArea.left, changeArea.bottom, changeArea.right, bufferSize); + const auto page = _pages.ActivePage(); + const auto changeRect = _CalculateRectArea(page, changeArea.top, changeArea.left, changeArea.bottom, changeArea.right); const auto lineCount = changeRect.height(); // If the change extent is rectangular, we can apply the change with a // single call. The same is true for a stream extent that is only one line. if (_modes.test(Mode::RectangularChangeExtent) || lineCount == 1) { - _ChangeRectAttributes(textBuffer, changeRect, changeOps); + _ChangeRectAttributes(page, changeRect, changeOps); } // If the stream extent is more than one line we require three passes. The // top line is altered from the left offset up to the end of the line. The @@ -1032,10 +1036,10 @@ void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, c // must be greater than the left, otherwise the operation is ignored. else if (lineCount > 1 && changeRect.right > changeRect.left) { - const auto bufferWidth = bufferSize.width; - _ChangeRectAttributes(textBuffer, { changeRect.origin(), til::size{ bufferWidth - changeRect.left, 1 } }, changeOps); - _ChangeRectAttributes(textBuffer, { { 0, changeRect.top + 1 }, til::size{ bufferWidth, lineCount - 2 } }, changeOps); - _ChangeRectAttributes(textBuffer, { { 0, changeRect.bottom - 1 }, til::size{ changeRect.right, 1 } }, changeOps); + const auto pageWidth = page.Width(); + _ChangeRectAttributes(page, { changeRect.origin(), til::size{ pageWidth - changeRect.left, 1 } }, changeOps); + _ChangeRectAttributes(page, { { 0, changeRect.top + 1 }, til::size{ pageWidth, lineCount - 2 } }, changeOps); + _ChangeRectAttributes(page, { { 0, changeRect.bottom - 1 }, til::size{ changeRect.right, 1 } }, changeOps); } } @@ -1043,25 +1047,26 @@ void AdaptDispatch::_ChangeRectOrStreamAttributes(const til::rect& changeArea, c // - Helper method to calculate the applicable buffer coordinates for use with // the various rectangular area operations. // Arguments: +// - page - The target page. // - top - The first row of the area. // - left - The first column of the area. // - bottom - The last row of the area (inclusive). // - right - The last column of the area (inclusive). -// - bufferSize - The size of the target buffer. // Return value: // - An exclusive rect with the absolute buffer coordinates. -til::rect AdaptDispatch::_CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize) +til::rect AdaptDispatch::_CalculateRectArea(const Page& page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - const auto viewport = _api.GetViewport(); + const auto pageWidth = page.Width(); + const auto pageHeight = page.Height(); // We start by calculating the margin offsets and maximum dimensions. - // If the origin mode isn't set, we use the viewport extent. - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, false); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferSize.width); + // If the origin mode isn't set, we use the page extent. + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, false); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth); const auto yOffset = _modes.test(Mode::Origin) ? topMargin : 0; - const auto yMaximum = _modes.test(Mode::Origin) ? bottomMargin + 1 : viewport.height(); + const auto yMaximum = _modes.test(Mode::Origin) ? bottomMargin + 1 : pageHeight; const auto xOffset = _modes.test(Mode::Origin) ? leftMargin : 0; - const auto xMaximum = _modes.test(Mode::Origin) ? rightMargin + 1 : bufferSize.width; + const auto xMaximum = _modes.test(Mode::Origin) ? rightMargin + 1 : pageWidth; auto fillRect = til::inclusive_rect{}; fillRect.left = left + xOffset; @@ -1077,9 +1082,9 @@ til::rect AdaptDispatch::_CalculateRectArea(const VTInt top, const VTInt left, c fillRect.top = std::min(fillRect.top, yMaximum) - 1; fillRect.bottom = std::min(fillRect.bottom, yMaximum) - 1; - // To get absolute coordinates we offset with the viewport top. - fillRect.top += viewport.top; - fillRect.bottom += viewport.top; + // To get absolute coordinates we offset with the page top. + fillRect.top += page.Top(); + fillRect.bottom += page.Top(); return til::rect{ fillRect }; } @@ -1198,25 +1203,22 @@ bool AdaptDispatch::ReverseAttributesRectangularArea(const VTInt top, const VTIn // - left - The first column of the source area. // - bottom - The last row of the source area (inclusive). // - right - The last column of the source area (inclusive). -// - page - The source page number (unused for now). +// - page - The source page number. // - dstTop - The first row of the destination. // - dstLeft - The first column of the destination. -// - dstPage - The destination page number (unused for now). +// - dstPage - The destination page number. // Return Value: // - True. -bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt /*page*/, const VTInt dstTop, const VTInt dstLeft, const VTInt /*dstPage*/) +bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const VTInt page, const VTInt dstTop, const VTInt dstLeft, const VTInt dstPage) { - // GH#13892 We don't yet support the paging extension, so for now we ignore - // the page parameters. This is the same as if the maximum page count was 1. - - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize().Dimensions(); - const auto srcRect = _CalculateRectArea(top, left, bottom, right, bufferSize); + const auto src = _pages.Get(page); + const auto dst = _pages.Get(dstPage); + const auto srcRect = _CalculateRectArea(src, top, left, bottom, right); const auto dstBottom = dstTop + srcRect.height() - 1; const auto dstRight = dstLeft + srcRect.width() - 1; - const auto dstRect = _CalculateRectArea(dstTop, dstLeft, dstBottom, dstRight, bufferSize); + const auto dstRect = _CalculateRectArea(dst, dstTop, dstLeft, dstBottom, dstRight); - if (dstRect && dstRect.origin() != srcRect.origin()) + if (dstRect && (dstRect.origin() != srcRect.origin() || src.Number() != dst.Number())) { // If the source is bigger than the available space at the destination // it needs to be clipped, so we only care about the destination size. @@ -1228,18 +1230,18 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const // Note that we read two cells from the source before we start writing // to the target, so a two-cell DBCS character can't accidentally delete // itself when moving one cell horizontally. - auto next = OutputCell(*textBuffer.GetCellDataAt(srcPos)); + auto next = OutputCell(*src.Buffer().GetCellDataAt(srcPos)); do { const auto current = next; const auto currentSrcPos = srcPos; srcView.WalkInBounds(srcPos, walkDirection); - next = OutputCell(*textBuffer.GetCellDataAt(srcPos)); + next = OutputCell(*src.Buffer().GetCellDataAt(srcPos)); // If the source position is offscreen (which can occur on double // width lines), then we shouldn't copy anything to the destination. - if (currentSrcPos.x < textBuffer.GetLineWidth(currentSrcPos.y)) + if (currentSrcPos.x < src.Buffer().GetLineWidth(currentSrcPos.y)) { - textBuffer.WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos); + dst.Buffer().WriteLine(OutputCellIterator({ ¤t, 1 }), dstPos); } } while (dstView.WalkInBounds(dstPos, walkDirection)); _api.NotifyAccessibilityChange(dstRect); @@ -1261,8 +1263,8 @@ bool AdaptDispatch::CopyRectangularArea(const VTInt top, const VTInt left, const // - True. bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - auto& textBuffer = _api.GetTextBuffer(); - const auto fillRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); + const auto page = _pages.ActivePage(); + const auto fillRect = _CalculateRectArea(page, top, left, bottom, right); // The standard only allows for characters in the range of the GL and GR // character set tables, but we also support additional Unicode characters @@ -1274,8 +1276,8 @@ bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c if (glChar || grChar || unicodeChar) { const auto fillChar = _termOutput.TranslateKey(gsl::narrow_cast(charValue)); - const auto& fillAttributes = textBuffer.GetCurrentAttributes(); - _FillRect(textBuffer, fillRect, { &fillChar, 1 }, fillAttributes); + const auto& fillAttributes = page.Attributes(); + _FillRect(page, fillRect, { &fillChar, 1 }, fillAttributes); } return true; @@ -1293,10 +1295,10 @@ bool AdaptDispatch::FillRectangularArea(const VTParameter ch, const VTInt top, c // - True. bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - auto& textBuffer = _api.GetTextBuffer(); - const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, eraseRect, whitespace, eraseAttributes); + const auto page = _pages.ActivePage(); + const auto eraseRect = _CalculateRectArea(page, top, left, bottom, right); + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, eraseRect, whitespace, eraseAttributes); return true; } @@ -1312,9 +1314,9 @@ bool AdaptDispatch::EraseRectangularArea(const VTInt top, const VTInt left, cons // - True. bool AdaptDispatch::SelectiveEraseRectangularArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right) { - auto& textBuffer = _api.GetTextBuffer(); - const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); - _SelectiveEraseRect(textBuffer, eraseRect); + const auto page = _pages.ActivePage(); + const auto eraseRect = _CalculateRectArea(page, top, left, bottom, right); + _SelectiveEraseRect(page, eraseRect); return true; } @@ -1346,7 +1348,7 @@ bool AdaptDispatch::SelectAttributeChangeExtent(const DispatchTypes::ChangeExten // the buffer memory. // Arguments: // - id - a numeric label used to identify the request. -// - page - The page number (unused for now). +// - page - The page number. // - top - The first row of the area. // - left - The first column of the area. // - bottom - The last row of the area (inclusive). @@ -1359,7 +1361,9 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p // If this feature is not enabled, we'll just report a zero checksum. if constexpr (Feature_VtChecksumReport::IsEnabled()) { - if (page == 1) + // If the page number is 0, then we're meant to return a checksum of all + // of the pages, but we have no need for that, so we'll just return 0. + if (page != 0) { // As part of the checksum, we need to include the color indices of each // cell, and in the case of default colors, those indices come from the @@ -1370,8 +1374,8 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p defaultFgIndex = defaultFgIndex < 16 ? defaultFgIndex : 7; defaultBgIndex = defaultBgIndex < 16 ? defaultBgIndex : 0; - const auto& textBuffer = _api.GetTextBuffer(); - const auto eraseRect = _CalculateRectArea(top, left, bottom, right, textBuffer.GetSize().Dimensions()); + const auto target = _pages.Get(page); + const auto eraseRect = _CalculateRectArea(target, top, left, bottom, right); for (auto row = eraseRect.top; row < eraseRect.bottom; row++) { for (auto col = eraseRect.left; col < eraseRect.right; col++) @@ -1381,7 +1385,7 @@ bool AdaptDispatch::RequestChecksumRectangularArea(const VTInt id, const VTInt p // predate Unicode, though, so we'd need a custom mapping table // to lookup the correct checksums. Considering this is only for // testing at the moment, that doesn't seem worth the effort. - const auto cell = textBuffer.GetCellDataAt({ col, row }); + const auto cell = target.Buffer().GetCellDataAt({ col, row }); for (auto ch : cell->Chars()) { // That said, I've made a special allowance for U+2426, @@ -1431,14 +1435,14 @@ bool AdaptDispatch::SetLineRendition(const LineRendition rendition) // The line rendition can't be changed if left/right margins are allowed. if (!_modes.test(Mode::AllowDECSLRM)) { - auto& textBuffer = _api.GetTextBuffer(); - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - textBuffer.SetCurrentLineRendition(rendition, eraseAttributes); + const auto page = _pages.ActivePage(); + const auto eraseAttributes = _GetEraseAttributes(page); + page.Buffer().SetCurrentLineRendition(rendition, eraseAttributes); // There is some variation in how this was handled by the different DEC // terminals, but the STD 070 reference (on page D-13) makes it clear that // the delayed wrap (aka the Last Column Flag) was expected to be reset when // line rendition controls were executed. - textBuffer.GetCursor().ResetDelayEOLWrap(); + page.Cursor().ResetDelayEOLWrap(); } return true; } @@ -1630,7 +1634,7 @@ void AdaptDispatch::_DeviceStatusReport(const wchar_t* parameters) const } // Routine Description: -// - CPR and DECXCPR- Reports the current cursor position within the viewport, +// - CPR and DECXCPR- Reports the current cursor position within the page, // as well as the current page number if this is an extended report. // Arguments: // - extendedReport - Set to true if the report should include the page number @@ -1638,32 +1642,30 @@ void AdaptDispatch::_DeviceStatusReport(const wchar_t* parameters) const // - void AdaptDispatch::_CursorPositionReport(const bool extendedReport) { - const auto viewport = _api.GetViewport(); - const auto& textBuffer = _api.GetTextBuffer(); + const auto page = _pages.ActivePage(); // First pull the cursor position relative to the entire buffer out of the console. - til::point cursorPosition{ textBuffer.GetCursor().GetPosition() }; + til::point cursorPosition{ page.Cursor().GetPosition() }; - // Now adjust it for its position in respect to the current viewport top. - cursorPosition.y -= viewport.top; + // Now adjust it for its position in respect to the current page top. + cursorPosition.y -= page.Top(); - // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1. + // NOTE: 1,1 is the top-left corner of the page in VT-speak, so add 1. cursorPosition.x++; cursorPosition.y++; // If the origin mode is set, the cursor is relative to the margin origin. if (_modes.test(Mode::Origin)) { - cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; - cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + cursorPosition.x -= _GetHorizontalMargins(page.Width()).first; + cursorPosition.y -= _GetVerticalMargins(page, false).first; } // Now send it back into the input channel of the console. if (extendedReport) { - // An extended report should also include the page number, but for now - // we hard-code it to 1, since we don't yet support paging (GH#13892). - const auto pageNumber = 1; + // An extended report also includes the page number. + const auto pageNumber = page.Number(); const auto response = wil::str_printf(L"\x1b[?%d;%d;%dR", cursorPosition.y, cursorPosition.x, pageNumber); _api.ReturnResponse(response); } @@ -1711,12 +1713,10 @@ void AdaptDispatch::_MacroChecksumReport(const VTParameter id) const // - void AdaptDispatch::_ScrollMovement(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); + const auto page = _pages.ActivePage(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + _ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); } // Routine Description: @@ -1743,6 +1743,88 @@ bool AdaptDispatch::ScrollDown(const VTInt uiDistance) return true; } +// Routine Description: +// - NP - Moves the active position one or more pages ahead, and moves the +// cursor to home. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::NextPage(const VTInt pageCount) +{ + PagePositionRelative(pageCount); + return CursorPosition(1, 1); +} + +// Routine Description: +// - PP - Moves the active position one or more pages back, and moves the +// cursor to home. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::PrecedingPage(const VTInt pageCount) +{ + PagePositionBack(pageCount); + return CursorPosition(1, 1); +} + +// Routine Description: +// - PPA - Moves the active position to the specified page number, without +// altering the cursor coordinates. +// Arguments: +// - page - Destination page +// Return Value: +// - True. +bool AdaptDispatch::PagePositionAbsolute(const VTInt page) +{ + _pages.MoveTo(page, _modes.test(Mode::PageCursorCoupling)); + return true; +} + +// Routine Description: +// - PPR - Moves the active position one or more pages ahead, without altering +// the cursor coordinates. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::PagePositionRelative(const VTInt pageCount) +{ + _pages.MoveRelative(pageCount, _modes.test(Mode::PageCursorCoupling)); + return true; +} + +// Routine Description: +// - PPB - Moves the active position one or more pages back, without altering +// the cursor coordinates. +// Arguments: +// - pageCount - Number of pages to move +// Return Value: +// - True. +bool AdaptDispatch::PagePositionBack(const VTInt pageCount) +{ + _pages.MoveRelative(-pageCount, _modes.test(Mode::PageCursorCoupling)); + return true; +} + +// Routine Description: +// - DECRQDE - Requests the area of page memory that is currently visible. +// Arguments: +// - None +// Return Value: +// - True. +bool AdaptDispatch::RequestDisplayedExtent() +{ + const auto page = _pages.VisiblePage(); + const auto width = page.Viewport().width(); + const auto height = page.Viewport().height(); + const auto left = page.XPanOffset() + 1; + const auto top = page.YPanOffset() + 1; + _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[{};{};{};{};{}\"w"), height, width, left, top, page.Number())); + return true; +} + // Routine Description: // - DECCOLM not only sets the number of columns, but also clears the screen buffer, // resets the page margins and origin mode, and places the cursor at 1,1 @@ -1755,10 +1837,10 @@ void AdaptDispatch::_SetColumnMode(const bool enable) // Only proceed if DECCOLM is allowed. Return true, as this is technically a successful handling. if (_modes.test(Mode::AllowDECCOLM) && !_api.IsConsolePty()) { - const auto viewport = _api.GetViewport(); - const auto viewportHeight = viewport.bottom - viewport.top; - const auto viewportWidth = (enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns); - _api.ResizeWindow(viewportWidth, viewportHeight); + const auto page = _pages.VisiblePage(); + const auto pageHeight = page.Height(); + const auto pageWidth = (enable ? DispatchTypes::s_sDECCOLMSetColumns : DispatchTypes::s_sDECCOLMResetColumns); + _api.ResizeWindow(pageWidth, pageHeight); _modes.set(Mode::Column, enable); _modes.reset(Mode::Origin, Mode::AllowDECSLRM); CursorPosition(1, 1); @@ -1781,8 +1863,8 @@ void AdaptDispatch::_SetAlternateScreenBufferMode(const bool enable) if (enable) { CursorSaveState(); - const auto& textBuffer = _api.GetTextBuffer(); - _api.UseAlternateScreenBuffer(_GetEraseAttributes(textBuffer)); + const auto page = _pages.ActivePage(); + _api.UseAlternateScreenBuffer(_GetEraseAttributes(page)); _usingAltBuffer = true; } else @@ -1860,21 +1942,28 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con // Resetting DECAWM should also reset the delayed wrap flag. if (!enable) { - _api.GetTextBuffer().GetCursor().ResetDelayEOLWrap(); + _pages.ActivePage().Cursor().ResetDelayEOLWrap(); } return true; case DispatchTypes::ModeParams::DECARM_AutoRepeatMode: _terminalInput.SetInputMode(TerminalInput::Mode::AutoRepeat, enable); return !_PassThroughInputModes(); case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - _api.GetTextBuffer().GetCursor().SetBlinkingAllowed(enable); + _pages.ActivePage().Cursor().SetBlinkingAllowed(enable); return !_api.IsConsolePty(); case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: - _api.GetTextBuffer().GetCursor().SetIsVisible(enable); + _pages.ActivePage().Cursor().SetIsVisible(enable); return true; case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport: _modes.set(Mode::AllowDECCOLM, enable); return true; + case DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode: + _modes.set(Mode::PageCursorCoupling, enable); + if (enable) + { + _pages.MakeActivePageVisible(); + } + return true; case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode: _terminalInput.SetInputMode(TerminalInput::Mode::Keypad, enable); return !_PassThroughInputModes(); @@ -1887,9 +1976,8 @@ bool AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con if (enable) { // If we've allowed left/right margins, we can't have line renditions. - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); + const auto page = _pages.ActivePage(); + page.Buffer().ResetLineRenditionRange(page.Top(), page.Bottom()); } return true; case DispatchTypes::ModeParams::DECECM_EraseColorMode: @@ -2011,10 +2099,10 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) enabled = _terminalInput.GetInputMode(TerminalInput::Mode::AutoRepeat); break; case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - enabled = _api.GetTextBuffer().GetCursor().IsBlinkingAllowed(); + enabled = _pages.ActivePage().Cursor().IsBlinkingAllowed(); break; case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: - enabled = _api.GetTextBuffer().GetCursor().IsVisible(); + enabled = _pages.ActivePage().Cursor().IsVisible(); break; case DispatchTypes::ModeParams::XTERM_EnableDECCOLMSupport: // DECCOLM is not supported in conpty mode @@ -2023,6 +2111,9 @@ bool AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) enabled = _modes.test(Mode::AllowDECCOLM); } break; + case DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode: + enabled = _modes.test(Mode::PageCursorCoupling); + break; case DispatchTypes::ModeParams::DECNKM_NumericKeypadMode: enabled = _terminalInput.GetInputMode(TerminalInput::Mode::Keypad); break; @@ -2100,20 +2191,17 @@ bool AdaptDispatch::SetKeypadMode(const bool fApplicationMode) // - void AdaptDispatch::_InsertDeleteLineHelper(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); const auto col = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin) { // We emulate inserting and deleting by scrolling the area between the cursor and the bottom margin. - _ScrollRectVertically(textBuffer, { leftMargin, row, rightMargin + 1, bottomMargin + 1 }, delta); + _ScrollRectVertically(page, { leftMargin, row, rightMargin + 1, bottomMargin + 1 }, delta); // The IL and DL controls are also expected to move the cursor to the left margin. cursor.SetXPosition(leftMargin); @@ -2161,20 +2249,17 @@ bool AdaptDispatch::DeleteLine(const VTInt distance) // - void AdaptDispatch::_InsertDeleteColumnHelper(const VTInt delta) { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); - - const auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + const auto& cursor = page.Cursor(); const auto col = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); if (row >= topMargin && row <= bottomMargin && col >= leftMargin && col <= rightMargin) { // We emulate inserting and deleting by scrolling the area between the cursor and the right margin. - _ScrollRectHorizontally(textBuffer, { col, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); + _ScrollRectHorizontally(page, { col, topMargin, rightMargin + 1, bottomMargin + 1 }, delta); } } @@ -2246,23 +2331,23 @@ void AdaptDispatch::_DoSetTopBottomScrollingMargins(const VTInt topMargin, til::CoordType actualTop = topMargin; til::CoordType actualBottom = bottomMargin; - const auto viewport = _api.GetViewport(); - const auto screenHeight = viewport.bottom - viewport.top; + const auto page = _pages.ActivePage(); + const auto pageHeight = page.Height(); // The default top margin is line 1 if (actualTop == 0) { actualTop = 1; } - // The default bottom margin is the screen height + // The default bottom margin is the page height if (actualBottom == 0) { - actualBottom = screenHeight; + actualBottom = pageHeight; } // The top margin must be less than the bottom margin, and the - // bottom margin must be less than or equal to the screen height - if (actualTop < actualBottom && actualBottom <= screenHeight) + // bottom margin must be less than or equal to the page height + if (actualTop < actualBottom && actualBottom <= pageHeight) { - if (actualTop == 1 && actualBottom == screenHeight) + if (actualTop == 1 && actualBottom == pageHeight) { // Client requests setting margins to the entire screen // - clear them instead of setting them. @@ -2323,23 +2408,23 @@ void AdaptDispatch::_DoSetLeftRightScrollingMargins(const VTInt leftMargin, til::CoordType actualLeft = leftMargin; til::CoordType actualRight = rightMargin; - const auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Width(); + const auto page = _pages.ActivePage(); + const auto pageWidth = page.Width(); // The default left margin is column 1 if (actualLeft == 0) { actualLeft = 1; } - // The default right margin is the buffer width + // The default right margin is the page width if (actualRight == 0) { - actualRight = bufferWidth; + actualRight = pageWidth; } // The left margin must be less than the right margin, and the // right margin must be less than or equal to the buffer width - if (actualLeft < actualRight && actualRight <= bufferWidth) + if (actualLeft < actualRight && actualRight <= pageWidth) { - if (actualLeft == 1 && actualRight == bufferWidth) + if (actualLeft == 1 && actualRight == pageWidth) { // Client requests setting margins to the entire screen // - clear them instead of setting them. @@ -2416,20 +2501,20 @@ bool AdaptDispatch::CarriageReturn() // Routine Description: // - Helper method for executing a line feed, possibly preceded by carriage return. // Arguments: -// - textBuffer - Target buffer on which the line feed is executed. +// - page - Target page on which the line feed is executed. // - withReturn - Set to true if a carriage return should be performed as well. // - wrapForced - Set to true is the line feed was the result of the line wrapping. // Return Value: // - -void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced) +void AdaptDispatch::_DoLineFeed(const Page& page, const bool withReturn, const bool wrapForced) { - const auto viewport = _api.GetViewport(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto bufferHeight = textBuffer.GetSize().Height(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); + auto& textBuffer = page.Buffer(); + const auto pageWidth = page.Width(); + const auto bufferHeight = page.BufferHeight(); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(pageWidth); - auto& cursor = textBuffer.GetCursor(); + auto& cursor = page.Cursor(); const auto currentPosition = cursor.GetPosition(); auto newPosition = currentPosition; @@ -2451,36 +2536,36 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c { // If we're not at the bottom margin, or outside the horizontal margins, // then there's no scrolling, so we make sure we don't move past the - // bottom of the viewport. - newPosition.y = std::min(currentPosition.y + 1, viewport.bottom - 1); + // bottom of the page. + newPosition.y = std::min(currentPosition.y + 1, page.Bottom() - 1); newPosition = textBuffer.ClampPositionWithinLine(newPosition); } - else if (topMargin > viewport.top || leftMargin > 0 || rightMargin < bufferWidth - 1) + else if (topMargin > page.Top() || leftMargin > 0 || rightMargin < pageWidth - 1) { - // If the top margin isn't at the top of the viewport, or the + // If the top margin isn't at the top of the page, or the // horizontal margins are set, then we're just scrolling the margin // area and the cursor stays where it is. - _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); + _ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); } - else if (viewport.bottom < bufferHeight) + else if (page.Bottom() < bufferHeight) { - // If the top margin is at the top of the viewport, then we'll scroll + // If the top margin is at the top of the page, then we'll scroll // the content up by panning the viewport down, and also move the cursor // down a row. But we only do this if the viewport hasn't yet reached // the end of the buffer. - _api.SetViewportPosition({ viewport.left, viewport.top + 1 }); + _api.SetViewportPosition({ page.XPanOffset(), page.Top() + 1 }); newPosition.y++; - // And if the bottom margin didn't cover the full viewport, we copy the - // lower part of the viewport down so it remains static. But for a full + // And if the bottom margin didn't cover the full page, we copy the + // lower part of the page down so it remains static. But for a full // pan we reset the newly revealed row with the erase attributes. - if (bottomMargin < viewport.bottom - 1) + if (bottomMargin < page.Bottom() - 1) { - _ScrollRectVertically(textBuffer, { 0, bottomMargin + 1, bufferWidth, viewport.bottom + 1 }, 1); + _ScrollRectVertically(page, { 0, bottomMargin + 1, pageWidth, page.Bottom() + 1 }, 1); } else { - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); textBuffer.GetMutableRowByOffset(newPosition.y).Reset(eraseAttributes); } } @@ -2489,7 +2574,7 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c // If the viewport has reached the end of the buffer, we can't pan down, // so we cycle the row coordinates, which effectively scrolls the buffer // content up. In this case we don't need to move the cursor down. - const auto eraseAttributes = _GetEraseAttributes(textBuffer); + const auto eraseAttributes = _GetEraseAttributes(page); textBuffer.IncrementCircularBuffer(eraseAttributes); _api.NotifyBufferRotation(1); @@ -2499,11 +2584,11 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c cursor.SetIsOn(false); textBuffer.TriggerScroll({ 0, -1 }); - // And again, if the bottom margin didn't cover the full viewport, we - // copy the lower part of the viewport down so it remains static. - if (bottomMargin < viewport.bottom - 1) + // And again, if the bottom margin didn't cover the full page, we + // copy the lower part of the page down so it remains static. + if (bottomMargin < page.Bottom() - 1) { - _ScrollRectVertically(textBuffer, { 0, bottomMargin, bufferWidth, bufferHeight }, 1); + _ScrollRectVertically(page, { 0, bottomMargin, pageWidth, bufferHeight }, 1); } } @@ -2520,17 +2605,17 @@ void AdaptDispatch::_DoLineFeed(TextBuffer& textBuffer, const bool withReturn, c // - True if handled successfully. False otherwise. bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType) { - auto& textBuffer = _api.GetTextBuffer(); + const auto page = _pages.ActivePage(); switch (lineFeedType) { case DispatchTypes::LineFeedType::DependsOnMode: - _DoLineFeed(textBuffer, _api.GetSystemMode(ITerminalApi::Mode::LineFeed), false); + _DoLineFeed(page, _api.GetSystemMode(ITerminalApi::Mode::LineFeed), false); return true; case DispatchTypes::LineFeedType::WithoutReturn: - _DoLineFeed(textBuffer, false, false); + _DoLineFeed(page, false, false); return true; case DispatchTypes::LineFeedType::WithReturn: - _DoLineFeed(textBuffer, true, false); + _DoLineFeed(page, true, false); return true; default: return false; @@ -2546,23 +2631,22 @@ bool AdaptDispatch::LineFeed(const DispatchTypes::LineFeedType lineFeedType) // - True. bool AdaptDispatch::ReverseLineFeed() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + const auto& textBuffer = page.Buffer(); + auto& cursor = page.Cursor(); const auto cursorPosition = cursor.GetPosition(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); // If the cursor is at the top of the margin area, we shift the buffer // contents down, to emulate inserting a line at that point. if (cursorPosition.y == topMargin && cursorPosition.x >= leftMargin && cursorPosition.x <= rightMargin) { - _ScrollRectVertically(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); + _ScrollRectVertically(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); } - else if (cursorPosition.y > viewport.top) + else if (cursorPosition.y > page.Top()) { - // Otherwise we move the cursor up, but not past the top of the viewport. + // Otherwise we move the cursor up, but not past the top of the page. cursor.SetPosition(textBuffer.ClampPositionWithinLine({ cursorPosition.x, cursorPosition.y - 1 })); _ApplyCursorMovementFlags(cursor); } @@ -2578,18 +2662,16 @@ bool AdaptDispatch::ReverseLineFeed() // - True. bool AdaptDispatch::BackIndex() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); const auto cursorPosition = cursor.GetPosition(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); // If the cursor is at the left of the margin area, we shift the buffer right. if (cursorPosition.x == leftMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) { - _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); + _ScrollRectHorizontally(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, 1); } // Otherwise we move the cursor left, but not past the start of the line. else if (cursorPosition.x > 0) @@ -2609,21 +2691,19 @@ bool AdaptDispatch::BackIndex() // - True. bool AdaptDispatch::ForwardIndex() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); const auto cursorPosition = cursor.GetPosition(); - const auto bufferWidth = textBuffer.GetSize().Width(); - const auto [leftMargin, rightMargin] = _GetHorizontalMargins(bufferWidth); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [leftMargin, rightMargin] = _GetHorizontalMargins(page.Width()); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); // If the cursor is at the right of the margin area, we shift the buffer left. if (cursorPosition.x == rightMargin && cursorPosition.y >= topMargin && cursorPosition.y <= bottomMargin) { - _ScrollRectHorizontally(textBuffer, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); + _ScrollRectHorizontally(page, { leftMargin, topMargin, rightMargin + 1, bottomMargin + 1 }, -1); } // Otherwise we move the cursor right, but not past the end of the line. - else if (cursorPosition.x < textBuffer.GetLineWidth(cursorPosition.y) - 1) + else if (cursorPosition.x < page.Buffer().GetLineWidth(cursorPosition.y) - 1) { cursor.SetXPosition(cursorPosition.x + 1); _ApplyCursorMovementFlags(cursor); @@ -2651,11 +2731,10 @@ bool AdaptDispatch::SetWindowTitle(std::wstring_view title) // - True. bool AdaptDispatch::HorizontalTabSet() { - const auto& textBuffer = _api.GetTextBuffer(); - const auto width = textBuffer.GetSize().Dimensions().width; - const auto column = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto column = page.Cursor().GetPosition().x; - _InitTabStopsForWidth(width); + _InitTabStopsForWidth(page.Width()); _tabStopColumns.at(column) = true; return true; @@ -2672,15 +2751,14 @@ bool AdaptDispatch::HorizontalTabSet() // - True. bool AdaptDispatch::ForwardTab(const VTInt numTabs) { - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); auto column = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto width = textBuffer.GetLineWidth(row); + const auto width = page.Buffer().GetLineWidth(row); auto tabsPerformed = 0; - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width); const auto clampToMargin = row >= topMargin && row <= bottomMargin && column <= rightMargin; const auto maxColumn = clampToMargin ? rightMargin : width - 1; @@ -2721,15 +2799,14 @@ bool AdaptDispatch::ForwardTab(const VTInt numTabs) // - True. bool AdaptDispatch::BackwardsTab(const VTInt numTabs) { - auto& textBuffer = _api.GetTextBuffer(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.ActivePage(); + auto& cursor = page.Cursor(); auto column = cursor.GetPosition().x; const auto row = cursor.GetPosition().y; - const auto width = textBuffer.GetLineWidth(row); + const auto width = page.Buffer().GetLineWidth(row); auto tabsPerformed = 0; - const auto viewport = _api.GetViewport(); - const auto [topMargin, bottomMargin] = _GetVerticalMargins(viewport, true); + const auto [topMargin, bottomMargin] = _GetVerticalMargins(page, true); const auto [leftMargin, rightMargin] = _GetHorizontalMargins(width); const auto clampToMargin = row >= topMargin && row <= bottomMargin && column >= leftMargin; const auto minColumn = clampToMargin ? leftMargin : 0; @@ -2780,11 +2857,10 @@ bool AdaptDispatch::TabClear(const DispatchTypes::TabClearType clearType) // - void AdaptDispatch::_ClearSingleTabStop() { - const auto& textBuffer = _api.GetTextBuffer(); - const auto width = textBuffer.GetSize().Dimensions().width; - const auto column = textBuffer.GetCursor().GetPosition().x; + const auto page = _pages.ActivePage(); + const auto column = page.Cursor().GetPosition().x; - _InitTabStopsForWidth(width); + _InitTabStopsForWidth(page.Width()); _tabStopColumns.at(column) = false; } @@ -3021,7 +3097,7 @@ bool AdaptDispatch::AnnounceCodeStructure(const VTInt ansiLevel) // True if handled successfully. False otherwise. bool AdaptDispatch::SoftReset() { - _api.GetTextBuffer().GetCursor().SetIsVisible(true); // Cursor enabled. + _pages.ActivePage().Cursor().SetIsVisible(true); // Cursor enabled. // Replace mode; Absolute cursor addressing; Disallow left/right margins. _modes.reset(Mode::InsertReplace, Mode::Origin, Mode::AllowDECSLRM); @@ -3086,6 +3162,9 @@ bool AdaptDispatch::HardReset() _usingAltBuffer = false; } + // Reset all page buffers. + _pages.Reset(); + // Completely reset the TerminalOutput state. _termOutput = {}; if (_initialCodePage.has_value()) @@ -3126,7 +3205,7 @@ bool AdaptDispatch::HardReset() _api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, false); // Restore cursor blinking mode. - _api.GetTextBuffer().GetCursor().SetBlinkingAllowed(true); + _pages.ActivePage().Cursor().SetBlinkingAllowed(true); // Delete all current tab stops and reapply TabSet(DispatchTypes::TabSetType::SetEvery8Columns); @@ -3136,7 +3215,7 @@ bool AdaptDispatch::HardReset() _fontBuffer = nullptr; // Reset internal modes to their initial state - _modes = {}; + _modes = { Mode::PageCursorCoupling }; // Clear and release the macro buffer. if (_macroBuffer) @@ -3172,18 +3251,16 @@ bool AdaptDispatch::HardReset() // - True. bool AdaptDispatch::ScreenAlignmentPattern() { - const auto viewport = _api.GetViewport(); - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferWidth = textBuffer.GetSize().Dimensions().width; + const auto page = _pages.ActivePage(); // Fill the screen with the letter E using the default attributes. - _FillRect(textBuffer, { 0, viewport.top, bufferWidth, viewport.bottom }, L"E", {}); + _FillRect(page, { 0, page.Top(), page.Width(), page.Bottom() }, L"E", {}); // Reset the line rendition for all of these rows. - textBuffer.ResetLineRenditionRange(viewport.top, viewport.bottom); + page.Buffer().ResetLineRenditionRange(page.Top(), page.Bottom()); // Reset the meta/extended attributes (but leave the colors unchanged). - auto attr = textBuffer.GetCurrentAttributes(); + auto attr = page.Attributes(); attr.SetStandardErase(); - _api.SetTextAttributes(attr); + page.SetAttributes(attr); // Reset the origin mode to absolute, and disallow left/right margins. _modes.reset(Mode::Origin, Mode::AllowDECSLRM); // Clear the scrolling margins. @@ -3198,8 +3275,8 @@ bool AdaptDispatch::ScreenAlignmentPattern() //Routine Description: // - Erase Scrollback (^[[3J - ED extension by xterm) // Because conhost doesn't exactly have a scrollback, We have to be tricky here. -// We need to move the entire viewport to 0,0, and clear everything outside -// (0, 0, viewportWidth, viewportHeight) To give the appearance that +// We need to move the entire page to 0,0, and clear everything outside +// (0, 0, pageWidth, pageHeight) To give the appearance that // everything above the viewport was cleared. // We don't want to save the text BELOW the viewport, because in *nix, there isn't anything there // (There isn't a scroll-forward, only a scrollback) @@ -3209,19 +3286,15 @@ bool AdaptDispatch::ScreenAlignmentPattern() // - True if handled successfully. False otherwise. bool AdaptDispatch::_EraseScrollback() { - const auto viewport = _api.GetViewport(); - const auto top = viewport.top; - const auto height = viewport.bottom - viewport.top; - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize().Dimensions(); - auto& cursor = textBuffer.GetCursor(); + const auto page = _pages.VisiblePage(); + auto& cursor = page.Cursor(); const auto row = cursor.GetPosition().y; - textBuffer.ClearScrollback(top, height); + page.Buffer().ClearScrollback(page.Top(), page.Height()); // Move the viewport - _api.SetViewportPosition({ viewport.left, 0 }); + _api.SetViewportPosition({ page.XPanOffset(), 0 }); // Move the cursor to the same relative location. - cursor.SetYPosition(row - top); + cursor.SetYPosition(row - page.Top()); cursor.SetHasMoved(true); // GH#2715 - If this succeeded, but we're in a conpty, return `false` to @@ -3245,25 +3318,26 @@ bool AdaptDispatch::_EraseScrollback() // - True if handled successfully. False otherwise. bool AdaptDispatch::_EraseAll() { - const auto viewport = _api.GetViewport(); - const auto viewportHeight = viewport.bottom - viewport.top; - auto& textBuffer = _api.GetTextBuffer(); - const auto bufferSize = textBuffer.GetSize(); + const auto page = _pages.ActivePage(); + const auto pageWidth = page.Width(); + const auto pageHeight = page.Height(); + const auto bufferHeight = page.BufferHeight(); + auto& textBuffer = page.Buffer(); const auto inPtyMode = _api.IsConsolePty(); - // Stash away the current position of the cursor within the viewport. + // Stash away the current position of the cursor within the page. // We'll need to restore the cursor to that same relative position, after // we move the viewport. - auto& cursor = textBuffer.GetCursor(); - const auto row = cursor.GetPosition().y - viewport.top; + auto& cursor = page.Cursor(); + const auto row = cursor.GetPosition().y - page.Top(); - // Calculate new viewport position. Typically we want to move one line below + // Calculate new page position. Typically we want to move one line below // the last non-space row, but if the last non-space character is the very // start of the buffer, then we shouldn't move down at all. const auto lastChar = textBuffer.GetLastNonSpaceCharacter(); - auto newViewportTop = lastChar == til::point{} ? 0 : lastChar.y + 1; - auto newViewportBottom = newViewportTop + viewportHeight; - const auto delta = newViewportBottom - (bufferSize.Height()); + auto newPageTop = lastChar == til::point{} ? 0 : lastChar.y + 1; + auto newPageBottom = newPageTop + pageHeight; + const auto delta = newPageBottom - bufferHeight; if (delta > 0) { for (auto i = 0; i < delta; i++) @@ -3271,8 +3345,8 @@ bool AdaptDispatch::_EraseAll() textBuffer.IncrementCircularBuffer(); } _api.NotifyBufferRotation(delta); - newViewportTop -= delta; - newViewportBottom -= delta; + newPageTop -= delta; + newPageBottom -= delta; // We don't want to trigger a scroll in pty mode, because we're going to // pass through the ED sequence anyway, and this will just result in the // buffer being scrolled up by two pages instead of one. @@ -3281,26 +3355,31 @@ bool AdaptDispatch::_EraseAll() textBuffer.TriggerScroll({ 0, -delta }); } } - // Move the viewport - _api.SetViewportPosition({ viewport.left, newViewportTop }); + // Move the viewport if necessary. + if (newPageTop != page.Top()) + { + _api.SetViewportPosition({ page.XPanOffset(), newPageTop }); + } // Restore the relative cursor position - cursor.SetYPosition(row + newViewportTop); + cursor.SetYPosition(row + newPageTop); cursor.SetHasMoved(true); - // Erase all the rows in the current viewport. - const auto eraseAttributes = _GetEraseAttributes(textBuffer); - _FillRect(textBuffer, { 0, newViewportTop, bufferSize.Width(), newViewportBottom }, whitespace, eraseAttributes); + // Erase all the rows in the current page. + const auto eraseAttributes = _GetEraseAttributes(page); + _FillRect(page, { 0, newPageTop, pageWidth, newPageBottom }, whitespace, eraseAttributes); // Also reset the line rendition for the erased rows. - textBuffer.ResetLineRenditionRange(newViewportTop, newViewportBottom); + textBuffer.ResetLineRenditionRange(newPageTop, newPageBottom); // GH#5683 - If this succeeded, but we're in a conpty, return `false` to // make the state machine propagate this ED sequence to the connected // terminal application. While we're in conpty mode, when the client // requests a Erase All operation, we need to manually tell the // connected terminal to do the same thing, so that the terminal will - // move it's own buffer contents into the scrollback. - return !inPtyMode; + // move it's own buffer contents into the scrollback. But this only + // applies if we're in the active buffer, since this should have no + // visible effect for an inactive buffer. + return !(inPtyMode && textBuffer.IsActiveBuffer()); } //Routine Description: @@ -3353,7 +3432,7 @@ bool AdaptDispatch::SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle) return false; } - auto& cursor = _api.GetTextBuffer().GetCursor(); + auto& cursor = _pages.ActivePage().Cursor(); cursor.SetType(actualType); cursor.SetBlinkingAllowed(fEnableBlinking); @@ -3515,14 +3594,17 @@ bool AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationTy _api.ShowWindow(false); return true; case DispatchTypes::WindowManipulationType::RefreshWindow: - _api.GetTextBuffer().TriggerRedrawAll(); + _pages.VisiblePage().Buffer().TriggerRedrawAll(); return true; case DispatchTypes::WindowManipulationType::ResizeWindowInCharacters: _api.ResizeWindow(parameter2.value_or(0), parameter1.value_or(0)); return true; case DispatchTypes::WindowManipulationType::ReportTextSizeInCharacters: - _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[8;{};{}t"), _api.GetViewport().height(), _api.GetTextBuffer().GetSize().Width())); + { + const auto page = _pages.VisiblePage(); + _api.ReturnResponse(fmt::format(FMT_COMPILE(L"\033[8;{};{}t"), page.Height(), page.Width())); return true; + } default: return false; } @@ -3536,12 +3618,12 @@ bool AdaptDispatch::WindowManipulation(const DispatchTypes::WindowManipulationTy // - true bool AdaptDispatch::AddHyperlink(const std::wstring_view uri, const std::wstring_view params) { - auto& textBuffer = _api.GetTextBuffer(); - auto attr = textBuffer.GetCurrentAttributes(); - const auto id = textBuffer.GetHyperlinkId(uri, params); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); + const auto id = page.Buffer().GetHyperlinkId(uri, params); attr.SetHyperlinkId(id); - textBuffer.SetCurrentAttributes(attr); - textBuffer.AddHyperlinkToMap(uri, id); + page.SetAttributes(attr); + page.Buffer().AddHyperlinkToMap(uri, id); return true; } @@ -3551,10 +3633,10 @@ bool AdaptDispatch::AddHyperlink(const std::wstring_view uri, const std::wstring // - true bool AdaptDispatch::EndHyperlink() { - auto& textBuffer = _api.GetTextBuffer(); - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); attr.SetHyperlinkId(0); - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); return true; } @@ -3657,7 +3739,7 @@ bool AdaptDispatch::DoConEmuAction(const std::wstring_view string) // This seems like basically the same as 133;B - the end of the prompt, the start of the commandline. else if (subParam == 12) { - _api.GetTextBuffer().StartCommand(); + _pages.ActivePage().Buffer().StartCommand(); return true; } @@ -3701,7 +3783,7 @@ bool AdaptDispatch::DoITerm2Action(const std::wstring_view string) bool handled = false; if (action == L"SetMark") { - _api.GetTextBuffer().StartPrompt(); + _pages.ActivePage().Buffer().StartPrompt(); handled = true; } @@ -3747,19 +3829,19 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) { case L'A': // FTCS_PROMPT { - _api.GetTextBuffer().StartPrompt(); + _pages.ActivePage().Buffer().StartPrompt(); handled = true; break; } case L'B': // FTCS_COMMAND_START { - _api.GetTextBuffer().StartCommand(); + _pages.ActivePage().Buffer().StartCommand(); handled = true; break; } case L'C': // FTCS_COMMAND_EXECUTED { - _api.GetTextBuffer().StartOutput(); + _pages.ActivePage().Buffer().StartOutput(); handled = true; break; } @@ -3780,7 +3862,7 @@ bool AdaptDispatch::DoFinalTermAction(const std::wstring_view string) UINT_MAX; } - _api.GetTextBuffer().EndCurrentCommand(error); + _pages.ActivePage().Buffer().EndCurrentCommand(error); handled = true; break; @@ -4270,7 +4352,7 @@ void AdaptDispatch::_ReportSGRSetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r0"sv); - const auto& attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& attr = _pages.ActivePage().Attributes(); const auto ulStyle = attr.GetUnderlineStyle(); // For each boolean attribute that is set, we add the appropriate // parameter value to the response string. @@ -4339,8 +4421,8 @@ void AdaptDispatch::_ReportDECSTBMSetting() fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto viewport = _api.GetViewport(); - const auto [marginTop, marginBottom] = _GetVerticalMargins(viewport, false); + const auto page = _pages.ActivePage(); + const auto [marginTop, marginBottom] = _GetVerticalMargins(page, false); // VT origin is at 1,1 so we need to add 1 to these margins. fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginTop + 1, marginBottom + 1); @@ -4363,8 +4445,8 @@ void AdaptDispatch::_ReportDECSLRMSetting() fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto bufferWidth = _api.GetTextBuffer().GetSize().Width(); - const auto [marginLeft, marginRight] = _GetHorizontalMargins(bufferWidth); + const auto pageWidth = _pages.ActivePage().Width(); + const auto [marginLeft, marginRight] = _GetHorizontalMargins(pageWidth); // VT origin is at 1,1 so we need to add 1 to these margins. fmt::format_to(std::back_inserter(response), FMT_COMPILE(L"{};{}"), marginLeft + 1, marginRight + 1); @@ -4387,7 +4469,7 @@ void AdaptDispatch::_ReportDECSCASetting() const fmt::basic_memory_buffer response; response.append(L"\033P1$r"sv); - const auto& attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& attr = _pages.ActivePage().Attributes(); response.append(attr.IsProtected() ? L"1"sv : L"0"sv); // The '"q' indicates this is an DECSCA response, and ST ends the sequence. @@ -4505,31 +4587,27 @@ ITermDispatch::StringHandler AdaptDispatch::RestorePresentationState(const Dispa // - None void AdaptDispatch::_ReportCursorInformation() { - const auto viewport = _api.GetViewport(); - const auto& textBuffer = _api.GetTextBuffer(); - const auto& cursor = textBuffer.GetCursor(); - const auto& attributes = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + const auto& cursor = page.Cursor(); + const auto& attributes = page.Attributes(); // First pull the cursor position relative to the entire buffer out of the console. til::point cursorPosition{ cursor.GetPosition() }; - // Now adjust it for its position in respect to the current viewport top. - cursorPosition.y -= viewport.top; + // Now adjust it for its position in respect to the current page top. + cursorPosition.y -= page.Top(); - // NOTE: 1,1 is the top-left corner of the viewport in VT-speak, so add 1. + // NOTE: 1,1 is the top-left corner of the page in VT-speak, so add 1. cursorPosition.x++; cursorPosition.y++; // If the origin mode is set, the cursor is relative to the margin origin. if (_modes.test(Mode::Origin)) { - cursorPosition.x -= _GetHorizontalMargins(textBuffer.GetSize().Width()).first; - cursorPosition.y -= _GetVerticalMargins(viewport, false).first; + cursorPosition.x -= _GetHorizontalMargins(page.Width()).first; + cursorPosition.y -= _GetVerticalMargins(page, false).first; } - // Paging is not supported yet (GH#13892). - const auto pageNumber = 1; - // Only some of the rendition attributes are reported. // Bit Attribute // 1 bold @@ -4575,7 +4653,7 @@ void AdaptDispatch::_ReportCursorInformation() FMT_COMPILE(L"\033P1$u{};{};{};{};{};{};{};{};{};{}{}{}{}\033\\"), cursorPosition.y, cursorPosition.x, - pageNumber, + page.Number(), renditionAttributes, characterAttributes, flags, @@ -4612,7 +4690,6 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() VTParameter row{}; VTParameter column{}; }; - auto& textBuffer = _api.GetTextBuffer(); return [&, state = State{}](const auto ch) mutable { if (numeric.test(state.field)) { @@ -4634,7 +4711,7 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() } else if (state.field == Field::Page) { - // Paging is not supported yet (GH#13892). + PagePositionAbsolute(state.value); } else if (state.field == Field::GL && state.value <= 3) { @@ -4659,19 +4736,21 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() state.value = ch; if (state.field == Field::SGR) { - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); attr.SetIntense(state.value & 1); attr.SetUnderlineStyle(state.value & 2 ? UnderlineStyle::SinglyUnderlined : UnderlineStyle::NoUnderline); attr.SetBlinking(state.value & 4); attr.SetReverseVideo(state.value & 8); attr.SetInvisible(state.value & 16); - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); } else if (state.field == Field::Attr) { - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); attr.SetProtected(state.value & 1); - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); } else if (state.field == Field::Sizes) { @@ -4697,7 +4776,8 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreCursorInformation() // above, so we only need to worry about setting it. if (delayedEOLWrap) { - textBuffer.GetCursor().DelayEOLWrap(); + const auto page = _pages.ActivePage(); + page.Cursor().DelayEOLWrap(); } } } @@ -4744,7 +4824,7 @@ void AdaptDispatch::_ReportTabStops() // In order to be compatible with the original hardware terminals, we only // report tab stops up to the current buffer width, even though there may // be positions recorded beyond that limit. - const auto width = _api.GetTextBuffer().GetSize().Dimensions().width; + const auto width = _pages.ActivePage().Width(); _InitTabStopsForWidth(width); using namespace std::string_view_literals; @@ -4780,7 +4860,7 @@ ITermDispatch::StringHandler AdaptDispatch::_RestoreTabStops() // In order to be compatible with the original hardware terminals, we need // to be able to set tab stops up to at least 132 columns, even though the // current buffer width may be less than that. - const auto width = std::max(_api.GetTextBuffer().GetSize().Dimensions().width, 132); + const auto width = std::max(_pages.ActivePage().Width(), 132); _ClearAllTabStops(); _InitTabStopsForWidth(width); diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index 36bb0312145..dbea2fcd887 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -18,6 +18,7 @@ Author(s): #include "ITerminalApi.hpp" #include "FontBuffer.hpp" #include "MacroBuffer.hpp" +#include "PageManager.hpp" #include "terminalOutput.hpp" #include "../input/terminalInput.hpp" #include "../../types/inc/sgrStack.hpp" @@ -81,6 +82,12 @@ namespace Microsoft::Console::VirtualTerminal bool RequestTerminalParameters(const DispatchTypes::ReportingPermission permission) override; // DECREQTPARM bool ScrollUp(const VTInt distance) override; // SU bool ScrollDown(const VTInt distance) override; // SD + bool NextPage(const VTInt pageCount) override; // NP + bool PrecedingPage(const VTInt pageCount) override; // PP + bool PagePositionAbsolute(const VTInt page) override; // PPA + bool PagePositionRelative(const VTInt pageCount) override; // PPR + bool PagePositionBack(const VTInt pageCount) override; // PPB + bool RequestDisplayedExtent() override; // DECRQDE bool InsertLine(const VTInt distance) override; // IL bool DeleteLine(const VTInt distance) override; // DL bool InsertColumn(const VTInt distance) override; // DECIC @@ -178,7 +185,8 @@ namespace Microsoft::Console::VirtualTerminal AllowDECCOLM, AllowDECSLRM, EraseColor, - RectangularChangeExtent + RectangularChangeExtent, + PageCursorCoupling }; enum class ScrollDirection { @@ -189,6 +197,7 @@ namespace Microsoft::Console::VirtualTerminal { VTInt Row = 1; VTInt Column = 1; + VTInt Page = 1; bool IsDelayedEOLWrap = false; bool IsOriginModeRelative = false; TextAttribute Attributes = {}; @@ -214,20 +223,20 @@ namespace Microsoft::Console::VirtualTerminal }; void _WriteToBuffer(const std::wstring_view string); - std::pair _GetVerticalMargins(const til::rect& viewport, const bool absolute) noexcept; + std::pair _GetVerticalMargins(const Page& page, const bool absolute) noexcept; std::pair _GetHorizontalMargins(const til::CoordType bufferWidth) noexcept; bool _CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins); void _ApplyCursorMovementFlags(Cursor& cursor) noexcept; - void _FillRect(TextBuffer& textBuffer, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const; - void _SelectiveEraseRect(TextBuffer& textBuffer, const til::rect& eraseRect); - void _ChangeRectAttributes(TextBuffer& textBuffer, const til::rect& changeRect, const ChangeOps& changeOps); + void _FillRect(const Page& page, const til::rect& fillRect, const std::wstring_view& fillChar, const TextAttribute& fillAttrs) const; + void _SelectiveEraseRect(const Page& page, const til::rect& eraseRect); + void _ChangeRectAttributes(const Page& page, const til::rect& changeRect, const ChangeOps& changeOps); void _ChangeRectOrStreamAttributes(const til::rect& changeArea, const ChangeOps& changeOps); - til::rect _CalculateRectArea(const VTInt top, const VTInt left, const VTInt bottom, const VTInt right, const til::size bufferSize); + til::rect _CalculateRectArea(const Page& page, const VTInt top, const VTInt left, const VTInt bottom, const VTInt right); bool _EraseScrollback(); bool _EraseAll(); - TextAttribute _GetEraseAttributes(const TextBuffer& textBuffer) const noexcept; - void _ScrollRectVertically(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); - void _ScrollRectHorizontally(TextBuffer& textBuffer, const til::rect& scrollRect, const VTInt delta); + TextAttribute _GetEraseAttributes(const Page& page) const noexcept; + void _ScrollRectVertically(const Page& page, const til::rect& scrollRect, const VTInt delta); + void _ScrollRectHorizontally(const Page& page, const til::rect& scrollRect, const VTInt delta); void _InsertDeleteCharacterHelper(const VTInt delta); void _InsertDeleteLineHelper(const VTInt delta); void _InsertDeleteColumnHelper(const VTInt delta); @@ -240,7 +249,7 @@ namespace Microsoft::Console::VirtualTerminal const VTInt rightMargin, const bool homeCursor = false); - void _DoLineFeed(TextBuffer& textBuffer, const bool withReturn, const bool wrapForced); + void _DoLineFeed(const Page& page, const bool withReturn, const bool wrapForced); void _DeviceStatusReport(const wchar_t* parameters) const; void _CursorPositionReport(const bool extendedReport); @@ -281,6 +290,7 @@ namespace Microsoft::Console::VirtualTerminal RenderSettings& _renderSettings; TerminalInput& _terminalInput; TerminalOutput _termOutput; + PageManager _pages; std::unique_ptr _fontBuffer; std::shared_ptr _macroBuffer; std::optional _initialCodePage; @@ -295,7 +305,7 @@ namespace Microsoft::Console::VirtualTerminal til::inclusive_rect _scrollMargins; - til::enumset _modes; + til::enumset _modes{ Mode::PageCursorCoupling }; SgrStack _sgrStack; diff --git a/src/terminal/adapter/adaptDispatchGraphics.cpp b/src/terminal/adapter/adaptDispatchGraphics.cpp index ac1b0180eff..f7dab41cd1c 100644 --- a/src/terminal/adapter/adaptDispatchGraphics.cpp +++ b/src/terminal/adapter/adaptDispatchGraphics.cpp @@ -422,9 +422,10 @@ void AdaptDispatch::_ApplyGraphicsOptions(const VTParameters options, // - True. bool AdaptDispatch::SetGraphicsRendition(const VTParameters options) { - auto attr = _api.GetTextBuffer().GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); _ApplyGraphicsOptions(options, attr); - _api.SetTextAttributes(attr); + page.SetAttributes(attr, &_api); return true; } @@ -438,8 +439,8 @@ bool AdaptDispatch::SetGraphicsRendition(const VTParameters options) // - True. bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) { - auto& textBuffer = _api.GetTextBuffer(); - auto attr = textBuffer.GetCurrentAttributes(); + const auto page = _pages.ActivePage(); + auto attr = page.Attributes(); for (size_t i = 0; i < options.size(); i++) { const LogicalAttributeOptions opt = options.at(i); @@ -456,7 +457,7 @@ bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) break; } } - textBuffer.SetCurrentAttributes(attr); + page.SetAttributes(attr); return true; } @@ -470,7 +471,7 @@ bool AdaptDispatch::SetCharacterProtectionAttribute(const VTParameters options) // - True. bool AdaptDispatch::PushGraphicsRendition(const VTParameters options) { - const auto& currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); + const auto& currentAttributes = _pages.ActivePage().Attributes(); _sgrStack.Push(currentAttributes, options); return true; } @@ -484,7 +485,8 @@ bool AdaptDispatch::PushGraphicsRendition(const VTParameters options) // - True. bool AdaptDispatch::PopGraphicsRendition() { - const auto& currentAttributes = _api.GetTextBuffer().GetCurrentAttributes(); - _api.SetTextAttributes(_sgrStack.Pop(currentAttributes)); + const auto page = _pages.ActivePage(); + const auto& currentAttributes = page.Attributes(); + page.SetAttributes(_sgrStack.Pop(currentAttributes), &_api); return true; } diff --git a/src/terminal/adapter/lib/adapter.vcxproj b/src/terminal/adapter/lib/adapter.vcxproj index 2780c818dfa..a05c8e5b83e 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj +++ b/src/terminal/adapter/lib/adapter.vcxproj @@ -15,6 +15,7 @@ + @@ -29,6 +30,7 @@ + diff --git a/src/terminal/adapter/lib/adapter.vcxproj.filters b/src/terminal/adapter/lib/adapter.vcxproj.filters index 798522c651f..501f6ce9bdc 100644 --- a/src/terminal/adapter/lib/adapter.vcxproj.filters +++ b/src/terminal/adapter/lib/adapter.vcxproj.filters @@ -36,6 +36,9 @@ Source Files + + Source Files + @@ -74,6 +77,9 @@ Header Files + + Header Files + diff --git a/src/terminal/adapter/sources.inc b/src/terminal/adapter/sources.inc index a7a07a29814..3ffd6bed7c7 100644 --- a/src/terminal/adapter/sources.inc +++ b/src/terminal/adapter/sources.inc @@ -34,6 +34,7 @@ SOURCES= \ ..\FontBuffer.cpp \ ..\InteractDispatch.cpp \ ..\MacroBuffer.cpp \ + ..\PageManager.cpp \ ..\adaptDispatchGraphics.cpp \ ..\terminalOutput.cpp \ diff --git a/src/terminal/adapter/termDispatch.hpp b/src/terminal/adapter/termDispatch.hpp index 82bb4f21320..3a1d19a0964 100644 --- a/src/terminal/adapter/termDispatch.hpp +++ b/src/terminal/adapter/termDispatch.hpp @@ -42,6 +42,12 @@ class Microsoft::Console::VirtualTerminal::TermDispatch : public Microsoft::Cons bool DeleteCharacter(const VTInt /*count*/) override { return false; } // DCH bool ScrollUp(const VTInt /*distance*/) override { return false; } // SU bool ScrollDown(const VTInt /*distance*/) override { return false; } // SD + bool NextPage(const VTInt /*pageCount*/) override { return false; } // NP + bool PrecedingPage(const VTInt /*pageCount*/) override { return false; } // PP + bool PagePositionAbsolute(const VTInt /*page*/) override { return false; } // PPA + bool PagePositionRelative(const VTInt /*pageCount*/) override { return false; } // PPR + bool PagePositionBack(const VTInt /*pageCount*/) override { return false; } // PPB + bool RequestDisplayedExtent() override { return false; } // DECRQDE bool InsertLine(const VTInt /*distance*/) override { return false; } // IL bool DeleteLine(const VTInt /*distance*/) override { return false; } // DL bool InsertColumn(const VTInt /*distance*/) override { return false; } // DECIC diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index 5cd5fbdc29e..c0ff50e5748 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -81,14 +81,10 @@ class TestGetSet final : public ITerminalApi return *_stateMachine; } - TextBuffer& GetTextBuffer() override + BufferState GetBufferAndViewport() override { - return *_textBuffer.get(); - } - - til::rect GetViewport() const override - { - return { _viewport.left, _viewport.top, _viewport.right, _viewport.bottom }; + const auto viewport = til::rect{ _viewport.left, _viewport.top, _viewport.right, _viewport.bottom }; + return { *_textBuffer.get(), viewport, true }; } void SetViewportPosition(const til::point /*position*/) override @@ -1575,14 +1571,23 @@ class AdapterTest coordCursorExpected.x++; coordCursorExpected.y++; - // Until we support paging (GH#13892) the reported page number should always be 1. - const auto pageExpected = 1; + // By default, the initial page number should be 1. + auto pageExpected = 1; VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExtendedCursorPositionReport, {})); wchar_t pwszBuffer[50]; swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.y, coordCursorExpected.x, pageExpected); _testGetSet->ValidateInputEvent(pwszBuffer); + + // Now test with the page number set to 3. + pageExpected = 3; + _pDispatch->PagePositionAbsolute(pageExpected); + + VERIFY_IS_TRUE(_pDispatch->DeviceStatusReport(DispatchTypes::StatusType::ExtendedCursorPositionReport, {})); + + swprintf_s(pwszBuffer, ARRAYSIZE(pwszBuffer), L"\x1b[?%d;%d;%dR", coordCursorExpected.y, coordCursorExpected.x, pageExpected); + _testGetSet->ValidateInputEvent(pwszBuffer); } TEST_METHOD(DeviceStatus_MacroSpaceReportTest) @@ -1746,6 +1751,42 @@ class AdapterTest VERIFY_THROWS(_pDispatch->TertiaryDeviceAttributes(), std::exception); } + TEST_METHOD(RequestDisplayedExtentTests) + { + Log::Comment(L"Starting test..."); + + Log::Comment(L"Test 1: Verify DECRQDE response in home position"); + _testGetSet->PrepData(); + _testGetSet->_viewport.left = 0; + _testGetSet->_viewport.right = 80; + _testGetSet->_viewport.top = 0; + _testGetSet->_viewport.bottom = 24; + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;1;1;1\"w"); + + Log::Comment(L"Test 2: Verify DECRQDE response when panned horizontally"); + _testGetSet->_viewport.left += 5; + _testGetSet->_viewport.right += 5; + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;1\"w"); + + Log::Comment(L"Test 3: Verify DECRQDE response on page 3"); + _pDispatch->PagePositionAbsolute(3); + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;3\"w"); + + Log::Comment(L"Test 3: Verify DECRQDE response when active page not visible"); + _pDispatch->ResetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + _pDispatch->PagePositionAbsolute(1); + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;3\"w"); + + Log::Comment(L"Test 4: Verify DECRQDE response when page 1 visible again"); + _pDispatch->SetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + VERIFY_IS_TRUE(_pDispatch->RequestDisplayedExtent()); + _testGetSet->ValidateInputEvent(L"\x1b[24;80;6;1;1\"w"); + } + TEST_METHOD(RequestTerminalParametersTests) { Log::Comment(L"Starting test..."); @@ -3263,7 +3304,7 @@ class AdapterTest setMacroText(63, L"Macro 63"); const auto getBufferOutput = [&]() { - const auto& textBuffer = _testGetSet->GetTextBuffer(); + const auto& textBuffer = _testGetSet->GetBufferAndViewport().buffer; const auto cursorPos = textBuffer.GetCursor().GetPosition(); return textBuffer.GetRowByOffset(cursorPos.y).GetText().substr(0, cursorPos.x); }; @@ -3314,7 +3355,8 @@ class AdapterTest { _testGetSet->PrepData(); _pDispatch->WindowManipulation(DispatchTypes::WindowManipulationType::ReportTextSizeInCharacters, NULL, NULL); - const std::wstring expectedResponse = fmt::format(L"\033[8;{};{}t", _testGetSet->GetViewport().height(), _testGetSet->GetTextBuffer().GetSize().Width()); + const auto [textBuffer, viewport, _] = _testGetSet->GetBufferAndViewport(); + const std::wstring expectedResponse = fmt::format(L"\033[8;{};{}t", viewport.height(), textBuffer.GetSize().Width()); _testGetSet->ValidateInputEvent(expectedResponse.c_str()); } @@ -3345,6 +3387,89 @@ class AdapterTest VERIFY_IS_TRUE(_pDispatch->DoVsCodeAction(LR"(Completions;10;20;30;{ "foo": "what;ever", "bar": 2 })")); } + TEST_METHOD(PageMovementTests) + { + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + auto& pages = _pDispatch->_pages; + const auto startPos = pages.ActivePage().Cursor().GetPosition(); + const auto homePos = til::point{ 0, pages.ActivePage().Top() }; + + // Testing PPA (page position absolute) + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"Initial page is 1"); + _pDispatch->PagePositionAbsolute(3); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PPA 3 moves to page 3"); + _pDispatch->PagePositionAbsolute(VTParameter{}); + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"PPA with omitted page moves to 1"); + _pDispatch->PagePositionAbsolute(9999); + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"PPA is clamped at page 6"); + VERIFY_ARE_EQUAL(startPos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position never changes"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + _pDispatch->PagePositionAbsolute(1); // Reset to page 1 + + // Testing PPR (page position relative) + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"Initial page is 1"); + _pDispatch->PagePositionRelative(2); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PPR 2 moves forward 2 pages"); + _pDispatch->PagePositionRelative(VTParameter{}); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"PPR with omitted count moves forward 1"); + _pDispatch->PagePositionRelative(9999); + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"PPR is clamped at page 6"); + VERIFY_ARE_EQUAL(startPos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position never changes"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Testing PPB (page position back) + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"Initial page is 6"); + _pDispatch->PagePositionBack(2); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"PPB 2 moves back 2 pages"); + _pDispatch->PagePositionBack(VTParameter{}); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PPB with omitted count moves back 1"); + _pDispatch->PagePositionBack(9999); + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"PPB is clamped at page 1"); + VERIFY_ARE_EQUAL(startPos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position never changes"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Testing NP (next page) + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"Initial page is 1"); + _pDispatch->NextPage(2); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"NP 2 moves forward 2 pages"); + _pDispatch->NextPage(VTParameter{}); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"NP with omitted count moves forward 1"); + _pDispatch->NextPage(9999); + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"NP is clamped at page 6"); + VERIFY_ARE_EQUAL(homePos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position is reset to home"); + + _testGetSet->PrepData(CursorX::XCENTER, CursorY::YCENTER); + + // Testing PP (preceding page) + VERIFY_ARE_EQUAL(6, pages.ActivePage().Number(), L"Initial page is 6"); + _pDispatch->PrecedingPage(2); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number(), L"PP 2 moves back 2 pages"); + _pDispatch->PrecedingPage(VTParameter{}); + VERIFY_ARE_EQUAL(3, pages.ActivePage().Number(), L"PP with omitted count moves back 1"); + _pDispatch->PrecedingPage(9999); + VERIFY_ARE_EQUAL(1, pages.ActivePage().Number(), L"PP is clamped at page 1"); + VERIFY_ARE_EQUAL(homePos, pages.ActivePage().Cursor().GetPosition(), L"Cursor position is reset to home"); + + // Testing DECPCCM (page cursor coupling mode) + _pDispatch->SetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + _pDispatch->PagePositionAbsolute(2); + VERIFY_ARE_EQUAL(2, pages.ActivePage().Number()); + VERIFY_ARE_EQUAL(2, pages.VisiblePage().Number(), L"Visible page should follow active if DECPCCM set"); + _pDispatch->ResetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + _pDispatch->PagePositionAbsolute(4); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number()); + VERIFY_ARE_EQUAL(2, pages.VisiblePage().Number(), L"Visible page should not change if DECPCCM reset"); + _pDispatch->SetMode(DispatchTypes::ModeParams::DECPCCM_PageCursorCouplingMode); + VERIFY_ARE_EQUAL(4, pages.ActivePage().Number()); + VERIFY_ARE_EQUAL(4, pages.VisiblePage().Number(), L"Active page should become visible when DECPCCM set"); + + // Reset to page 1 + _pDispatch->PagePositionAbsolute(1); + } + private: TerminalInput _terminalInput; std::unique_ptr _testGetSet; diff --git a/src/terminal/parser/OutputStateMachineEngine.cpp b/src/terminal/parser/OutputStateMachineEngine.cpp index f4b3fcbf677..7568f154677 100644 --- a/src/terminal/parser/OutputStateMachineEngine.cpp +++ b/src/terminal/parser/OutputStateMachineEngine.cpp @@ -556,6 +556,12 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete case CsiActionCodes::SD_ScrollDown: success = _dispatch->ScrollDown(parameters.at(0)); break; + case CsiActionCodes::NP_NextPage: + success = _dispatch->NextPage(parameters.at(0)); + break; + case CsiActionCodes::PP_PrecedingPage: + success = _dispatch->PrecedingPage(parameters.at(0)); + break; case CsiActionCodes::ANSISYSRC_CursorRestore: success = _dispatch->CursorRestoreState(); break; @@ -601,6 +607,15 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete } success = true; break; + case CsiActionCodes::PPA_PagePositionAbsolute: + success = _dispatch->PagePositionAbsolute(parameters.at(0)); + break; + case CsiActionCodes::PPR_PagePositionRelative: + success = _dispatch->PagePositionRelative(parameters.at(0)); + break; + case CsiActionCodes::PPB_PagePositionBack: + success = _dispatch->PagePositionBack(parameters.at(0)); + break; case CsiActionCodes::DECSCUSR_SetCursorStyle: success = _dispatch->SetCursorStyle(parameters.at(0)); break; @@ -610,6 +625,9 @@ bool OutputStateMachineEngine::ActionCsiDispatch(const VTID id, const VTParamete case CsiActionCodes::DECSCA_SetCharacterProtectionAttribute: success = _dispatch->SetCharacterProtectionAttribute(parameters); break; + case CsiActionCodes::DECRQDE_RequestDisplayedExtent: + success = _dispatch->RequestDisplayedExtent(); + break; case CsiActionCodes::XT_PushSgr: case CsiActionCodes::XT_PushSgrAlias: success = _dispatch->PushGraphicsRendition(parameters); diff --git a/src/terminal/parser/OutputStateMachineEngine.hpp b/src/terminal/parser/OutputStateMachineEngine.hpp index 64fbf81503e..0970e45dee2 100644 --- a/src/terminal/parser/OutputStateMachineEngine.hpp +++ b/src/terminal/parser/OutputStateMachineEngine.hpp @@ -122,6 +122,8 @@ namespace Microsoft::Console::VirtualTerminal DCH_DeleteCharacter = VTID("P"), SU_ScrollUp = VTID("S"), SD_ScrollDown = VTID("T"), + NP_NextPage = VTID("U"), + PP_PrecedingPage = VTID("V"), DECST8C_SetTabEvery8Columns = VTID("?W"), ECH_EraseCharacters = VTID("X"), CBT_CursorBackTab = VTID("Z"), @@ -147,9 +149,13 @@ namespace Microsoft::Console::VirtualTerminal DTTERM_WindowManipulation = VTID("t"), // NOTE: Overlaps with DECSLPP. Fix when/if implemented. ANSISYSRC_CursorRestore = VTID("u"), DECREQTPARM_RequestTerminalParameters = VTID("x"), + PPA_PagePositionAbsolute = VTID(" P"), + PPR_PagePositionRelative = VTID(" Q"), + PPB_PagePositionBack = VTID(" R"), DECSCUSR_SetCursorStyle = VTID(" q"), DECSTR_SoftReset = VTID("!p"), DECSCA_SetCharacterProtectionAttribute = VTID("\"q"), + DECRQDE_RequestDisplayedExtent = VTID("\"v"), XT_PushSgrAlias = VTID("#p"), XT_PopSgrAlias = VTID("#q"), XT_PushSgr = VTID("#{"), From e826203bb7e200f6f7029b6d8be68fb330c18735 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Mon, 20 May 2024 18:53:39 +0100 Subject: [PATCH 27/39] Remove some unused/obsolete XtermEngine code (#17287) ## Summary of the Pull Request The dirty view calculation in the `XtermEngine::StartPaint` method was originally used to detect a full frame paint that would require a clear screen, but that code was removed as part of PR #4741, making this calculation obsolete. The `performedSoftWrap` variable in the `XtermEngine::_MoveCursor` method was assumedly a remanent of some WIP code that was mistakenly committed in PR #5181. The variable was never actually used. ## Validation Steps Performed All the unit tests still pass and nothing seems obviously broken in manual testing. ## PR Checklist - [x] Closes #17280 --- src/renderer/vt/XtermEngine.cpp | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/renderer/vt/XtermEngine.cpp b/src/renderer/vt/XtermEngine.cpp index 4df161befc1..0dc9c8384e4 100644 --- a/src/renderer/vt/XtermEngine.cpp +++ b/src/renderer/vt/XtermEngine.cpp @@ -81,20 +81,6 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, _clearedAllThisFrame = true; _firstPaint = false; } - else - { - std::span dirty; - RETURN_IF_FAILED(GetDirtyArea(dirty)); - - // If we have 0 or 1 dirty pieces in the area, set as appropriate. - auto dirtyView = dirty.empty() ? Viewport::Empty() : Viewport::FromExclusive(til::at(dirty, 0)); - - // If there's more than 1, union them all up with the 1 we already have. - for (size_t i = 1; i < dirty.size(); ++i) - { - dirtyView = Viewport::Union(dirtyView, Viewport::FromExclusive(til::at(dirty, i))); - } - } return S_OK; } @@ -246,7 +232,6 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, auto hr = S_OK; const auto originalPos = _lastText; _trace.TraceMoveCursor(_lastText, coord); - auto performedSoftWrap = false; if (coord.x != _lastText.x || coord.y != _lastText.y) { if (coord.x == 0 && coord.y == 0) @@ -272,7 +257,6 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe, if (previousLineWrapped) { - performedSoftWrap = true; _trace.TraceWrapped(); hr = S_OK; } From 5d1cf1a704986808d09ae7729aaed39ce5d5f909 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Fri, 24 May 2024 11:28:28 -0700 Subject: [PATCH 28/39] Raise the dupe bot's threshold to .8 (#17318) I was talking with @plante-msft this week at Build and we agreed that .75 is just a bit too chatty. .8 seems like it's a better threshold - sure, it'll miss a few of the harder edge cases, but it'll chime in less frequently when it's just wrong. --- .github/workflows/similarIssues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/similarIssues.yml b/.github/workflows/similarIssues.yml index 3e377e2ff9d..290f83cbdcf 100644 --- a/.github/workflows/similarIssues.yml +++ b/.github/workflows/similarIssues.yml @@ -16,7 +16,7 @@ jobs: issueTitle: ${{ github.event.issue.title }} issueBody: ${{ github.event.issue.body }} repo: ${{ github.repository }} - similaritytolerance: "0.75" + similaritytolerance: "0.8" add-comment: needs: getSimilarIssues runs-on: ubuntu-latest From 212f43e7fb35ea2d64370d435034a961cf5cee1c Mon Sep 17 00:00:00 2001 From: Tushar Singh Date: Tue, 28 May 2024 22:30:55 +0530 Subject: [PATCH 29/39] Fix hyperlinks using an extra character at the end (#17326) Closes: #17323 ## Validation Steps Performed - Run `echo Hello ^(https://github.com/microsoft/terminal^)` in cmd. - Ctrl+click on the URL opens `https://github.com/microsoft/terminal` in the browser. - Hovering over the url in the terminal shows `https://github.com/microsoft/terminal` in the hover UI. --- src/cascadia/TerminalCore/Terminal.cpp | 4 +++ .../TerminalBufferTests.cpp | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 80f89a06320..e5641e2f6e1 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -522,6 +522,10 @@ std::wstring Terminal::GetHyperlinkAtBufferPosition(const til::point bufferPos) // Case 2 - Step 2: get the auto-detected hyperlink if (result.has_value() && result->value == _hyperlinkPatternId) { + // GetPlainText works with inclusive coordinates, but interval's stop + // point is (horizontally) exclusive, so let's just update it. + result->stop.x--; + return _activeBuffer().GetPlainText(result->start, result->stop); } return {}; diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp index 051732333cc..827c4da85ce 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalBufferTests.cpp @@ -51,6 +51,8 @@ class TerminalCoreUnitTests::TerminalBufferTests final TEST_METHOD(TestGetReverseTab); + TEST_METHOD(TestURLPatternDetection); + TEST_METHOD_SETUP(MethodSetup) { // STEP 1: Set up the Terminal @@ -594,3 +596,34 @@ void TerminalBufferTests::TestGetReverseTab() L"Cursor adjusted to last item in the sample list from position beyond end."); } } + +void TerminalBufferTests::TestURLPatternDetection() +{ + using namespace std::string_view_literals; + + constexpr auto BeforeStr = L""sv; + constexpr auto UrlStr = L"https://www.contoso.com"sv; + constexpr auto AfterStr = L""sv; + constexpr auto urlStartX = BeforeStr.size(); + constexpr auto urlEndX = BeforeStr.size() + UrlStr.size() - 1; + + auto& termSm = *term->_stateMachine; + termSm.ProcessString(fmt::format(FMT_COMPILE(L"{}{}{}"), BeforeStr, UrlStr, AfterStr)); + term->UpdatePatternsUnderLock(); + + std::wstring result; + + result = term->GetHyperlinkAtBufferPosition(til::point{ urlStartX - 1, 0 }); + VERIFY_IS_TRUE(result.empty(), L"URL is not detected before the actual URL."); + + result = term->GetHyperlinkAtBufferPosition(til::point{ urlStartX, 0 }); + VERIFY_IS_TRUE(!result.empty(), L"A URL is detected at the start position."); + VERIFY_ARE_EQUAL(result, UrlStr, L"Detected URL matches the given URL."); + + result = term->GetHyperlinkAtBufferPosition(til::point{ urlEndX, 0 }); + VERIFY_IS_TRUE(!result.empty(), L"A URL is detected at the end position."); + VERIFY_ARE_EQUAL(result, UrlStr, L"Detected URL matches the given URL."); + + result = term->GetHyperlinkAtBufferPosition(til::point{ urlEndX + 1, 0 }); + VERIFY_IS_TRUE(result.empty(), L"URL is not detected after the actual URL."); +} From 13de7c668577ae270ce9528d6a895fd169ece7cc Mon Sep 17 00:00:00 2001 From: Windows Console Service Bot <14666831+consvc@users.noreply.github.com> Date: Tue, 28 May 2024 12:29:23 -0500 Subject: [PATCH 30/39] Localization Updates - fix issues in Korean, Italian, Spanish (#17304) Closes #12665 Closes #12712 Closes #16867 --- .../Resources/es-ES/Resources.resw | 2 +- .../Resources/it-IT/Resources.resw | 6 ++-- .../Resources/it-IT/Resources.resw | 2 +- .../Resources/ko-KR/Resources.resw | 6 ++-- .../Resources/es-ES/Resources.resw | 28 +++++++++---------- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/cascadia/TerminalApp/Resources/es-ES/Resources.resw b/src/cascadia/TerminalApp/Resources/es-ES/Resources.resw index f0ce1d07a48..6ba64dbb17e 100644 --- a/src/cascadia/TerminalApp/Resources/es-ES/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/es-ES/Resources.resw @@ -205,7 +205,7 @@ Dividir pestaña - Panel dividido + Dividir panel Búsqueda en la web diff --git a/src/cascadia/TerminalApp/Resources/it-IT/Resources.resw b/src/cascadia/TerminalApp/Resources/it-IT/Resources.resw index 0f1b1a0b147..7c69fcee948 100644 --- a/src/cascadia/TerminalApp/Resources/it-IT/Resources.resw +++ b/src/cascadia/TerminalApp/Resources/it-IT/Resources.resw @@ -421,13 +421,13 @@ Apri nuova scheda - ALT + clic per dividere la finestra corrente + ALT+CLIC per dividere la finestra corrente - MAIUSC+clic per aprire una nuova finestra + MAIUSC+CLIC per aprire una nuova finestra - CTRL+Clic per aprire come amministratore + CTRL+CLIC per aprire come amministratore Chiudi diff --git a/src/cascadia/TerminalControl/Resources/it-IT/Resources.resw b/src/cascadia/TerminalControl/Resources/it-IT/Resources.resw index 3ebb2a21cb3..ba1f88ae074 100644 --- a/src/cascadia/TerminalControl/Resources/it-IT/Resources.resw +++ b/src/cascadia/TerminalControl/Resources/it-IT/Resources.resw @@ -176,7 +176,7 @@ Riprendi - CTRL+clic per aprire il collegamento + CTRL+CLIC per aprire il collegamento Nessun risultato diff --git a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw index 976717c8677..13a3f93b281 100644 --- a/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw +++ b/src/cascadia/TerminalSettingsEditor/Resources/ko-KR/Resources.resw @@ -1303,7 +1303,7 @@ This is the formal name for a font weight. - 밝게 + 가늘게 This is the formal name for a font weight. @@ -1315,7 +1315,7 @@ This is the formal name for a font weight. - 약간 굵은 + 약간 굵게 This is the formal name for a font weight. @@ -1323,7 +1323,7 @@ This is the formal name for a font weight. - 얇은 + 얇게 This is the formal name for a font weight. diff --git a/src/cascadia/TerminalSettingsModel/Resources/es-ES/Resources.resw b/src/cascadia/TerminalSettingsModel/Resources/es-ES/Resources.resw index c3c94c309ab..17dac88310f 100644 --- a/src/cascadia/TerminalSettingsModel/Resources/es-ES/Resources.resw +++ b/src/cascadia/TerminalSettingsModel/Resources/es-ES/Resources.resw @@ -124,7 +124,7 @@ Nueva pestaña - Panel dividido + Dividir panel Terminal (portátil) @@ -187,7 +187,7 @@ Cerrar todas las pestañas después de la pestaña actual - Cierre la ventana + Cerrar ventana Abrir menú del sistema @@ -276,10 +276,10 @@ Mover el foco al panel menor - Panel de intercambio + Intercambiar panel - {0} panel de intercambio + Intercambiar panel hacia {0} {0} will be replaced with one of the four directions "DirectionLeft", "DirectionRight", "DirectionUp", "DirectionDown" @@ -307,7 +307,7 @@ Identificar ventanas - Siguiente pestaña + Pestaña siguiente Abrir archivos de configuración y configuración predeterminada (JSON) @@ -357,24 +357,24 @@ {0} will be replaced with one of the four directions "DirectionLeft", "DirectionRight", "DirectionUp", or "DirectionDown" - Desplazar hacia abajo + Desplazarse hacia abajo - Desplazar hacia abajo {0} línea(s) + Desplazarse hacia abajo {0} línea(s) {0} will be replaced with the number of lines to scroll" - Desplácese hacia abajo una página + Desplazarse hacia abajo una página - Desplazar hacia arriba + Desplazarse hacia arriba - Desplazar hacia arriba {0} línea(s) + Desplazarse hacia arriba {0} línea(s) {0} will be replaced with the number of lines to scroll" - Desplácese hacia arriba una página + Desplazarse hacia arriba una página Desplazarse a la parte superior del historial @@ -429,7 +429,7 @@ Mover el panel a una nueva ventana - Panel dividido + Dividir panel Dividir el panel verticalmente @@ -487,7 +487,7 @@ Alternar la orientación de la división del panel - Zoom de panel de alternancia + Alternar zoom del panel Activar o desactivar el modo de solo lectura del panel @@ -524,7 +524,7 @@ Mostrar u ocultar la ventana de terminal - Mostrar u ocultar ventana Desastre + Mostrar u ocultar ventana Quake Quake is a well-known computer game and isn't related to earthquakes, etc. See https://en.wikipedia.org/wiki/Quake_(video_game) From 0bd0eeabcbe10b1307567e0dc04fc8a140565b58 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 29 May 2024 19:33:14 +0200 Subject: [PATCH 31/39] Cursor coordinates are viewport-relative (#17332) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The changeset is rather self-explanatory. Some things in the rendering code are in absolute and some things are in relative coordinates. Cursor coordinates belong to the latter. It's a bit confusing. Closes #17310 ## Validation Steps Performed * Use the GDI text renderer * Use cmd * Press and hold Enter * No more ghostly cursors ✅ --- src/renderer/base/renderer.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index c8ed8b28e7b..261b7f88cbf 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -432,7 +432,11 @@ bool Renderer::_CheckViewportAndScroll() // The cursor may have moved out of or into the viewport. Update the .inViewport property. { const auto view = ScreenToBufferLine(srNewViewport, _currentCursorOptions.lineRendition); - const auto coordCursor = _currentCursorOptions.coordCursor; + auto coordCursor = _currentCursorOptions.coordCursor; + + // `coordCursor` was stored in viewport-relative while `view` is in absolute coordinates. + // --> Turn it back into the absolute coordinates with the help of the old viewport. + coordCursor.y += srOldViewport.top; // Note that we allow the X coordinate to be outside the left border by 1 position, // because the cursor could still be visible if the focused character is double width. From a7c99beb6ba9ff971285143682001216ee8067f1 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Wed, 29 May 2024 16:30:08 -0700 Subject: [PATCH 32/39] Remove the animations from the command palette too (#17335) Exactly the same as #17247, but also just applied to the command palette. It's so much better guys. --- src/cascadia/TerminalApp/App.xaml | 19 +++++++++++++++++++ src/cascadia/TerminalApp/CommandPalette.xaml | 3 ++- .../TerminalApp/SuggestionsControl.xaml | 18 ------------------ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/cascadia/TerminalApp/App.xaml b/src/cascadia/TerminalApp/App.xaml index 1cda23ddd7c..cc0ce1517d5 100644 --- a/src/cascadia/TerminalApp/App.xaml +++ b/src/cascadia/TerminalApp/App.xaml @@ -146,6 +146,25 @@ + + + diff --git a/src/cascadia/TerminalApp/CommandPalette.xaml b/src/cascadia/TerminalApp/CommandPalette.xaml index 2e143a5f12c..4a3864adb75 100644 --- a/src/cascadia/TerminalApp/CommandPalette.xaml +++ b/src/cascadia/TerminalApp/CommandPalette.xaml @@ -403,7 +403,8 @@ ItemClick="_listItemClicked" ItemsSource="{x:Bind FilteredActions}" SelectionChanged="_listItemSelectionChanged" - SelectionMode="Single" /> + SelectionMode="Single" + Style="{StaticResource NoAnimationsPlease}" /> diff --git a/src/cascadia/TerminalApp/SuggestionsControl.xaml b/src/cascadia/TerminalApp/SuggestionsControl.xaml index b3e81865b3f..49525aa4ad0 100644 --- a/src/cascadia/TerminalApp/SuggestionsControl.xaml +++ b/src/cascadia/TerminalApp/SuggestionsControl.xaml @@ -100,24 +100,6 @@ GeneralItemTemplate="{StaticResource GeneralItemTemplate}" NestedItemTemplate="{StaticResource NestedItemTemplate}" /> - - From ad362fc8663f43f9a63aac76a05dd1f227fe2246 Mon Sep 17 00:00:00 2001 From: James Holderness Date: Thu, 30 May 2024 15:20:19 +0100 Subject: [PATCH 33/39] Position the conpty cursor correctly when wrappedRow is set (#17290) ## Summary of the Pull Request If the VT render engine is moving the cursor to the start of a row, and the previous row was marked as wrapped, it will assume that it doesn't need to do anything, because the next output should automatically move the cursor to the correct position anyway. However, if that cursor movement is coming from the final `PaintCursor` call for the frame, there isn't going to be any more output, so the cursor will be left in the wrong position. This PR fixes that issue by clearing the `_wrappedRow` field before the `_MoveCursor` call in the `PaintCursor` method. ## Validation Steps Performed I've confirmed that this fixes all the test cases mentioned in issue #17270, and issue #17013, and I've added a unit test to check the new behavior is working as expected. However, this change does break a couple of `ConptyRoundtripTests` that were expecting the terminal row to be marked as wrapped when writing a wrapped line in two parts using `WriteCharsLegacy`. This is because the legacy way of wrapping a line isn't the same as a VT delayed wrap, so it has to be emulated with cursor movement, and that can end up resetting the wrap flag. It's possible that could be fixed, but it's already broken in a number of other ways, so I don't think this makes things much worse. For now, I've just made the affected test cases skip the wrapping check. ## PR Checklist - [x] Closes #17013 - [x] Closes #17270 - [x] Tests added/passed --- .../ConptyRoundtripTests.cpp | 13 ++++++- src/host/ut_host/ConptyOutputTests.cpp | 37 +++++++++++++++++++ src/renderer/vt/paint.cpp | 9 +++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp index 2dc64562e88..42099a24e56 100644 --- a/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp +++ b/src/cascadia/UnitTests_TerminalCore/ConptyRoundtripTests.cpp @@ -3542,7 +3542,18 @@ void ConptyRoundtripTests::WrapNewLineAtBottomLikeMSYS() const auto actualNonSpacesAttrs = defaultAttrs; const auto actualSpacesAttrs = rowCircled || isTerminal ? defaultAttrs : conhostDefaultAttrs; - VERIFY_ARE_EQUAL(isWrapped, tb.GetRowByOffset(row).WasWrapForced()); + // When using WriteCharsLegacy to emit a wrapped line, with the + // frame painted before the second half of the wrapped line, the + // cursor needs to be manually moved to the second line, because + // that's what is expected of WriteCharsLegacy, and the terminal + // would otherwise delay that movement. But this means the line + // won't be marked as wrapped, and there's no easy way to fix that. + // For now we're just skipping this test. + if (!(writingMethod == PrintWithWriteCharsLegacy && paintEachNewline == PaintEveryLine && isWrapped)) + { + VERIFY_ARE_EQUAL(isWrapped, tb.GetRowByOffset(row).WasWrapForced()); + } + if (isWrapped) { TestUtils::VerifyExpectedString(tb, std::wstring(charsInFirstLine, L'~'), { 0, row }); diff --git a/src/host/ut_host/ConptyOutputTests.cpp b/src/host/ut_host/ConptyOutputTests.cpp index 1ebff63cac2..68ea772e4b3 100644 --- a/src/host/ut_host/ConptyOutputTests.cpp +++ b/src/host/ut_host/ConptyOutputTests.cpp @@ -122,6 +122,7 @@ class ConptyOutputTests TEST_METHOD(InvalidateUntilOneBeforeEnd); TEST_METHOD(SetConsoleTitleWithControlChars); TEST_METHOD(IncludeBackgroundColorChangesInFirstFrame); + TEST_METHOD(MoveCursorAfterWrapForced); private: bool _writeCallback(const char* const pch, const size_t cch); @@ -428,3 +429,39 @@ void ConptyOutputTests::IncludeBackgroundColorChangesInFirstFrame() VERIFY_SUCCEEDED(renderer.PaintFrame()); } + +void ConptyOutputTests::MoveCursorAfterWrapForced() +{ + auto& g = ServiceLocator::LocateGlobals(); + auto& renderer = *g.pRender; + auto& gci = g.getConsoleInformation(); + auto& si = gci.GetActiveOutputBuffer(); + auto& sm = si.GetStateMachine(); + + // We write a character in the rightmost column to trigger the _wrapForced + // flag. Technically this is a bug, but it's how things currently work. + sm.ProcessString(L"\x1b[1;999H*"); + + expectedOutput.push_back("\x1b[2J"); // standard init sequence for the first frame + expectedOutput.push_back("\x1b[m"); // standard init sequence for the first frame + expectedOutput.push_back("\x1b[1;80H"); + expectedOutput.push_back("*"); + expectedOutput.push_back("\x1b[?25h"); + + VERIFY_SUCCEEDED(renderer.PaintFrame()); + + // Position the cursor on line 2, and fill line 1 with A's. + sm.ProcessString(L"\x1b[2H"); + sm.ProcessString(L"\033[65;1;1;1;999$x"); + + expectedOutput.push_back("\x1b[H"); + expectedOutput.push_back("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); + + // The cursor must be explicitly moved to line 2 at the end of the frame. + // Although that may technically already be the next output location, we + // still need the cursor to be shown in that position when the frame ends. + expectedOutput.push_back("\r\n"); + expectedOutput.push_back("\x1b[?25h"); + + VERIFY_SUCCEEDED(renderer.PaintFrame()); +} diff --git a/src/renderer/vt/paint.cpp b/src/renderer/vt/paint.cpp index 1fcb5335887..69985b27eec 100644 --- a/src/renderer/vt/paint.cpp +++ b/src/renderer/vt/paint.cpp @@ -224,6 +224,15 @@ using namespace Microsoft::Console::Types; { _trace.TracePaintCursor(options.coordCursor); + // GH#17270: If the wrappedRow field is set, and the target cursor position + // is at the start of the next row, it's expected that any subsequent output + // would already be written to that location, so the _MoveCursor method may + // decide it doesn't need to do anything. In this case, though, we're not + // writing anything else, so the cursor will end up in the wrong location at + // the end of the frame. Clearing the wrappedRow field fixes that. + _wrappedRow = std::nullopt; + _trace.TraceClearWrapped(); + // MSFT:15933349 - Send the terminal the updated cursor information, if it's changed. LOG_IF_FAILED(_MoveCursor(options.coordCursor)); From bdc7c4fdbc8385bbf156fbac55f097117e1a1a3b Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 30 May 2024 16:25:02 +0200 Subject: [PATCH 34/39] Show parts of the scrollback on restore (#17334) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First, this makes use of `PSEUDOCONSOLE_INHERIT_CURSOR` to stop ConPTY from emitting a CSI 2 J on startup. Then, it uses `Terminal::SetViewportPosition` to fake-scroll the viewport down so that only 3 lines of scrollback are visible. It avoids printing actual newlines because if we later change the text buffer to actually track the written contents, we don't want those newlines to end up in the next buffer snapshot. Closes #17274 ## Validation Steps Performed * Restore cmd multiple times * There's always exactly 3 lines visible ✅ --- src/cascadia/TerminalApp/TerminalPage.cpp | 22 +++++++++++--------- src/cascadia/TerminalControl/ControlCore.cpp | 19 +++++++++++------ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp index ca86bb80a37..064d341b816 100644 --- a/src/cascadia/TerminalApp/TerminalPage.cpp +++ b/src/cascadia/TerminalApp/TerminalPage.cpp @@ -2994,15 +2994,6 @@ namespace winrt::TerminalApp::implementation const auto content = _manager.CreateCore(settings.DefaultSettings(), settings.UnfocusedSettings(), connection); const TermControl control{ content }; - - if (const auto id = settings.DefaultSettings().SessionId(); id != winrt::guid{}) - { - const auto settingsDir = CascadiaSettings::SettingsDirectory(); - const auto idStr = Utils::GuidToPlainString(id); - const auto path = fmt::format(FMT_COMPILE(L"{}\\buffer_{}.txt"), settingsDir, idStr); - control.RestoreFromPath(path); - } - return _SetupControl(control); } @@ -3102,7 +3093,10 @@ namespace winrt::TerminalApp::implementation return nullptr; } - auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profile, controlSettings.DefaultSettings(), false); + const auto sessionId = controlSettings.DefaultSettings().SessionId(); + const auto hasSessionId = sessionId != winrt::guid{}; + + auto connection = existingConnection ? existingConnection : _CreateConnectionFromSettings(profile, controlSettings.DefaultSettings(), hasSessionId); if (existingConnection) { connection.Resize(controlSettings.DefaultSettings().InitialRows(), controlSettings.DefaultSettings().InitialCols()); @@ -3124,6 +3118,14 @@ namespace winrt::TerminalApp::implementation const auto control = _CreateNewControlAndContent(controlSettings, connection); + if (hasSessionId) + { + const auto settingsDir = CascadiaSettings::SettingsDirectory(); + const auto idStr = Utils::GuidToPlainString(sessionId); + const auto path = fmt::format(FMT_COMPILE(L"{}\\buffer_{}.txt"), settingsDir, idStr); + control.RestoreFromPath(path); + } + auto paneContent{ winrt::make(profile, _terminalSettingsCache, control) }; auto resultPane = std::make_shared(paneContent); diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index c4ad4b0799d..d82bfac365c 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1787,12 +1787,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // * prints " [Restored - \ No newline at end of file + + Regular Expression + The tooltip text for the button on the search box control governing the use of "regular expressions" ("regex"). + + + Regular Expression Search + The accessibility description text for the button on the search box control governing the use of "regular expressions" ("regex"). + + + invalid + This brief message is displayed when a regular expression is invalid. + + diff --git a/src/cascadia/TerminalControl/SearchBoxControl.cpp b/src/cascadia/TerminalControl/SearchBoxControl.cpp index eaaae04add9..46e27de04d4 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.cpp +++ b/src/cascadia/TerminalControl/SearchBoxControl.cpp @@ -30,6 +30,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _focusableElements.insert(TextBox()); _focusableElements.insert(CloseButton()); _focusableElements.insert(CaseSensitivityButton()); + _focusableElements.insert(RegexButton()); _focusableElements.insert(GoForwardButton()); _focusableElements.insert(GoBackwardButton()); @@ -224,6 +225,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation return CaseSensitivityButton().IsChecked().GetBoolean(); } + bool SearchBoxControl::RegularExpression() + { + return RegexButton().IsChecked().GetBoolean(); + } + // Method Description: // - Handler for pressing Enter on TextBox, trigger // text search @@ -245,11 +251,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation const auto state = CoreWindow::GetForCurrentThread().GetKeyState(winrt::Windows::System::VirtualKey::Shift); if (WI_IsFlagSet(state, CoreVirtualKeyStates::Down)) { - Search.raise(Text(), !GoForward(), CaseSensitive()); + Search.raise(Text(), !GoForward(), CaseSensitive(), RegularExpression()); } else { - Search.raise(Text(), GoForward(), CaseSensitive()); + Search.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } e.Handled(true); } @@ -340,7 +346,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // kick off search - Search.raise(Text(), GoForward(), CaseSensitive()); + Search.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } // Method Description: @@ -361,7 +367,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation } // kick off search - Search.raise(Text(), GoForward(), CaseSensitive()); + Search.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } // Method Description: @@ -399,7 +405,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - void SearchBoxControl::TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) { - SearchChanged.raise(Text(), GoForward(), CaseSensitive()); + SearchChanged.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } // Method Description: @@ -411,7 +417,12 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - void SearchBoxControl::CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) { - SearchChanged.raise(Text(), GoForward(), CaseSensitive()); + SearchChanged.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); + } + + void SearchBoxControl::RegexButtonClicked(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::RoutedEventArgs const& /*e*/) + { + SearchChanged.raise(Text(), GoForward(), CaseSensitive(), RegularExpression()); } // Method Description: @@ -504,7 +515,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation double SearchBoxControl::_GetStatusMaxWidth() { const auto fontSize = StatusBox().FontSize(); - const auto maxLength = std::max({ _TextWidth(_FormatStatus(-1, -1), fontSize), + const auto maxLength = std::max({ _TextWidth(RS_(L"SearchRegexInvalid"), fontSize), + _TextWidth(_FormatStatus(-1, -1), fontSize), _TextWidth(_FormatStatus(0, -1), fontSize), _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus, MaximumTotalResultsToShowInStatus - 1), fontSize), _TextWidth(_FormatStatus(MaximumTotalResultsToShowInStatus + 1, MaximumTotalResultsToShowInStatus - 1), fontSize), @@ -521,9 +533,17 @@ namespace winrt::Microsoft::Terminal::Control::implementation // - currentMatch - the index of the current match (0-based) // Return Value: // - - void SearchBoxControl::SetStatus(int32_t totalMatches, int32_t currentMatch) + void SearchBoxControl::SetStatus(int32_t totalMatches, int32_t currentMatch, bool searchRegexInvalid) { - const auto status = _FormatStatus(totalMatches, currentMatch); + hstring status; + if (searchRegexInvalid) + { + status = RS_(L"SearchRegexInvalid"); + } + else + { + status = _FormatStatus(totalMatches, currentMatch); + } StatusBox().Text(status); } diff --git a/src/cascadia/TerminalControl/SearchBoxControl.h b/src/cascadia/TerminalControl/SearchBoxControl.h index 60ea00da481..d774e42b569 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.h +++ b/src/cascadia/TerminalControl/SearchBoxControl.h @@ -38,10 +38,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::hstring Text(); bool GoForward(); bool CaseSensitive(); + bool RegularExpression(); void SetFocusOnTextbox(); void PopulateTextbox(const winrt::hstring& text); bool ContainsFocus(); - void SetStatus(int32_t totalMatches, int32_t currentMatch); + void SetStatus(int32_t totalMatches, int32_t currentMatch, bool searchRegexInvalid); void ClearStatus(); void GoBackwardClicked(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Windows::UI::Xaml::RoutedEventArgs& /*e*/); @@ -50,6 +51,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TextBoxTextChanged(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); void CaseSensitivityButtonClicked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); + void RegexButtonClicked(winrt::Windows::Foundation::IInspectable const& sender, winrt::Windows::UI::Xaml::RoutedEventArgs const& e); void SearchBoxPointerPressedHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); void SearchBoxPointerReleasedHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/, winrt::Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e); diff --git a/src/cascadia/TerminalControl/SearchBoxControl.idl b/src/cascadia/TerminalControl/SearchBoxControl.idl index 3abf4f50229..61c133b1cce 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.idl +++ b/src/cascadia/TerminalControl/SearchBoxControl.idl @@ -3,7 +3,7 @@ namespace Microsoft.Terminal.Control { - delegate void SearchHandler(String query, Boolean goForward, Boolean isCaseSensitive); + delegate void SearchHandler(String query, Boolean goForward, Boolean isCaseSensitive, Boolean regularExpression); [default_interface] runtimeclass SearchBoxControl : Windows.UI.Xaml.Controls.UserControl, Windows.UI.Xaml.Data.INotifyPropertyChanged { @@ -11,7 +11,6 @@ namespace Microsoft.Terminal.Control void SetFocusOnTextbox(); void PopulateTextbox(String text); Boolean ContainsFocus(); - void SetStatus(Int32 totalMatches, Int32 currentMatch); void ClearStatus(); Windows.Foundation.Rect ContentClipRect{ get; }; Double OpenAnimationStartPoint{ get; }; diff --git a/src/cascadia/TerminalControl/SearchBoxControl.xaml b/src/cascadia/TerminalControl/SearchBoxControl.xaml index c17fcc3c010..7523cdcfc83 100644 --- a/src/cascadia/TerminalControl/SearchBoxControl.xaml +++ b/src/cascadia/TerminalControl/SearchBoxControl.xaml @@ -243,6 +243,17 @@ + + + +