From 1c234302ecee50aa7dc82ca937b0298784b303c8 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 23 Sep 2025 15:55:01 +0200 Subject: [PATCH 1/8] Move all blink handling into Renderer --- .github/actions/spelling/expect/expect.txt | 1 + src/buffer/out/cursor.cpp | 266 +++----- src/buffer/out/cursor.h | 55 +- src/cascadia/TerminalControl/ControlCore.cpp | 37 +- src/cascadia/TerminalControl/ControlCore.h | 5 - src/cascadia/TerminalControl/ControlCore.idl | 3 - src/cascadia/TerminalControl/HwndTerminal.cpp | 87 ++- src/cascadia/TerminalControl/HwndTerminal.hpp | 20 +- src/cascadia/TerminalControl/TermControl.cpp | 131 ---- src/cascadia/TerminalControl/TermControl.h | 5 - .../dll/Microsoft.Terminal.Control.def | 6 +- src/cascadia/TerminalCore/Terminal.cpp | 40 +- src/cascadia/TerminalCore/Terminal.hpp | 34 +- src/cascadia/TerminalCore/TerminalApi.cpp | 6 +- .../TerminalCore/TerminalSelection.cpp | 11 +- .../TerminalCore/terminalrenderdata.cpp | 55 +- .../WpfTerminalControl/NativeMethods.cs | 31 +- .../WpfTerminalControl/TerminalContainer.cs | 38 +- src/host/CursorBlinker.cpp | 146 ----- src/host/CursorBlinker.hpp | 37 -- src/host/_stream.cpp | 5 +- src/host/consoleInformation.cpp | 11 - src/host/ft_host/API_InputTests.cpp | 2 +- src/host/getset.cpp | 2 +- src/host/host-common.vcxitems | 2 - src/host/lib/hostlib.vcxproj.filters | 11 +- src/host/output.cpp | 15 - src/host/renderData.cpp | 135 +--- src/host/renderData.hpp | 65 +- src/host/screenInfo.cpp | 42 +- src/host/screenInfo.hpp | 6 +- src/host/selection.cpp | 7 +- src/host/selectionState.cpp | 1 - src/host/server.h | 3 - src/host/sources.inc | 1 - src/host/ut_host/ScreenBufferTests.cpp | 30 +- src/inc/til/small_vector.h | 11 + .../onecore/SystemConfigurationProvider.cpp | 12 +- .../onecore/SystemConfigurationProvider.hpp | 7 - .../win32/SystemConfigurationProvider.cpp | 2 +- .../win32/SystemConfigurationProvider.hpp | 3 - src/interactivity/win32/window.cpp | 3 - src/interactivity/win32/windowproc.cpp | 28 +- src/renderer/base/RenderSettings.cpp | 39 +- src/renderer/base/lib/base.vcxproj | 4 +- src/renderer/base/lib/base.vcxproj.filters | 9 +- src/renderer/base/renderer.cpp | 595 ++++++++++++++---- src/renderer/base/renderer.hpp | 99 ++- src/renderer/inc/IRenderData.hpp | 59 +- src/renderer/inc/RenderSettings.hpp | 8 +- src/terminal/adapter/adaptDispatch.cpp | 48 +- src/terminal/adapter/adaptDispatch.hpp | 1 - 52 files changed, 924 insertions(+), 1356 deletions(-) delete mode 100644 src/host/CursorBlinker.cpp delete mode 100644 src/host/CursorBlinker.hpp diff --git a/.github/actions/spelling/expect/expect.txt b/.github/actions/spelling/expect/expect.txt index c58c021ec9b..7e992bf05af 100644 --- a/.github/actions/spelling/expect/expect.txt +++ b/.github/actions/spelling/expect/expect.txt @@ -1765,6 +1765,7 @@ UINTs uld uldash uldb +ULONGLONG ulwave Unadvise unattend diff --git a/src/buffer/out/cursor.cpp b/src/buffer/out/cursor.cpp index 24ba784327a..04548d5505b 100644 --- a/src/buffer/out/cursor.cpp +++ b/src/buffer/out/cursor.cpp @@ -13,40 +13,28 @@ // - ulSize - The height of the cursor within this buffer Cursor::Cursor(const ULONG ulSize, TextBuffer& parentBuffer) noexcept : _parentBuffer{ parentBuffer }, - _fIsVisible(true), - _fIsOn(true), - _fIsDouble(false), - _fBlinkingAllowed(true), - _fDelay(false), - _fIsConversionArea(false), - _fDelayedEolWrap(false), - _fDeferCursorRedraw(false), - _fHaveDeferredCursorRedraw(false), - _ulSize(ulSize), - _cursorType(CursorType::Legacy) + _ulSize(ulSize) { } -Cursor::~Cursor() = default; - til::point Cursor::GetPosition() const noexcept { return _cPosition; } -bool Cursor::IsVisible() const noexcept +uint64_t Cursor::GetLastMutationId() const noexcept { - return _fIsVisible; + return _mutationId; } -bool Cursor::IsOn() const noexcept +bool Cursor::IsVisible() const noexcept { - return _fIsOn; + return _isVisible; } -bool Cursor::IsBlinkingAllowed() const noexcept +bool Cursor::IsBlinking() const noexcept { - return _fBlinkingAllowed; + return _isBlinking; } bool Cursor::IsDouble() const noexcept @@ -54,173 +42,128 @@ bool Cursor::IsDouble() const noexcept return _fIsDouble; } -bool Cursor::IsConversionArea() const noexcept -{ - return _fIsConversionArea; -} - -bool Cursor::GetDelay() const noexcept -{ - return _fDelay; -} - ULONG Cursor::GetSize() const noexcept { return _ulSize; } -void Cursor::SetIsVisible(const bool fIsVisible) noexcept -{ - _fIsVisible = fIsVisible; - _RedrawCursor(); -} - -void Cursor::SetIsOn(const bool fIsOn) noexcept +void Cursor::SetIsVisible(bool enable) { - _fIsOn = fIsOn; - _RedrawCursorAlways(); + if (_isVisible != enable) + { + _isVisible = enable; + _redrawIfVisible(); + } } -void Cursor::SetBlinkingAllowed(const bool fBlinkingAllowed) noexcept +void Cursor::SetIsBlinking(bool enable) { - _fBlinkingAllowed = fBlinkingAllowed; - // GH#2642 - From what we've gathered from other terminals, when blinking is - // disabled, the cursor should remain On always, and have the visibility - // controlled by the IsVisible property. So when you do a printf "\e[?12l" - // to disable blinking, the cursor stays stuck On. At this point, only the - // cursor visibility property controls whether the user can see it or not. - // (Yes, the cursor can be On and NOT Visible) - _fIsOn = true; - _RedrawCursorAlways(); + if (_isBlinking != enable) + { + _isBlinking = enable; + _redrawIfVisible(); + } } void Cursor::SetIsDouble(const bool fIsDouble) noexcept { - _fIsDouble = fIsDouble; - _RedrawCursor(); -} - -void Cursor::SetIsConversionArea(const bool fIsConversionArea) noexcept -{ - // Functionally the same as "Hide cursor" - // Never called with TRUE, it's only used in the creation of a - // ConversionAreaInfo, and never changed after that. - _fIsConversionArea = fIsConversionArea; - _RedrawCursorAlways(); -} - -void Cursor::SetDelay(const bool fDelay) noexcept -{ - _fDelay = fDelay; + if (_fIsDouble != fIsDouble) + { + _fIsDouble = fIsDouble; + _redrawIfVisible(); + } } void Cursor::SetSize(const ULONG ulSize) noexcept { - _ulSize = ulSize; - _RedrawCursor(); + if (_ulSize != ulSize) + { + _ulSize = ulSize; + _redrawIfVisible(); + } } void Cursor::SetStyle(const ULONG ulSize, const CursorType type) noexcept { - _ulSize = ulSize; - _cursorType = type; - - _RedrawCursor(); -} - -// Routine Description: -// - Sends a redraw message to the renderer only if the cursor is currently on. -// - NOTE: For use with most methods in this class. -// Arguments: -// - -// Return Value: -// - -void Cursor::_RedrawCursor() noexcept -{ - // Only trigger the redraw if we're on. - // Don't draw the cursor if this was triggered from a conversion area. - // (Conversion areas have cursors to mark the insertion point internally, but the user's actual cursor is the one on the primary screen buffer.) - if (IsOn() && !IsConversionArea()) + if (_ulSize != ulSize || _cursorType != type) { - if (_fDeferCursorRedraw) - { - _fHaveDeferredCursorRedraw = true; - } - else - { - _RedrawCursorAlways(); - } + _ulSize = ulSize; + _cursorType = type; + _redrawIfVisible(); } } -// Routine Description: -// - Sends a redraw message to the renderer no matter what. -// - NOTE: For use with the method that turns the cursor on and off to force a refresh -// and clear the ON cursor from the screen. Not for use with other methods. -// They should use the other method so refreshes are suppressed while the cursor is off. -// Arguments: -// - -// Return Value: -// - -void Cursor::_RedrawCursorAlways() noexcept -{ - _parentBuffer.NotifyPaintFrame(); -} - void Cursor::SetPosition(const til::point cPosition) noexcept { - _RedrawCursor(); - _cPosition = cPosition; - _RedrawCursor(); + // The VT code assumes that moving the cursor implicitly resets the delayed EOL wrap, + // so we call ResetDelayEOLWrap() independent of _cPosition != cPosition. + // You can see the effect of this with "`e[1;9999Ha`e[1;9999Hb", which should print just "b". ResetDelayEOLWrap(); + if (_cPosition != cPosition) + { + _cPosition = cPosition; + _redrawIfVisible(); + } } void Cursor::SetXPosition(const til::CoordType NewX) noexcept { - _RedrawCursor(); - _cPosition.x = NewX; - _RedrawCursor(); ResetDelayEOLWrap(); + if (_cPosition.x != NewX) + { + _cPosition.x = NewX; + _redrawIfVisible(); + } } void Cursor::SetYPosition(const til::CoordType NewY) noexcept { - _RedrawCursor(); - _cPosition.y = NewY; - _RedrawCursor(); ResetDelayEOLWrap(); + if (_cPosition.y != NewY) + { + _cPosition.y = NewY; + _redrawIfVisible(); + } } void Cursor::IncrementXPosition(const til::CoordType DeltaX) noexcept { - _RedrawCursor(); - _cPosition.x += DeltaX; - _RedrawCursor(); ResetDelayEOLWrap(); + if (DeltaX != 0) + { + _cPosition.x = _cPosition.x + DeltaX; + _redrawIfVisible(); + } } void Cursor::IncrementYPosition(const til::CoordType DeltaY) noexcept { - _RedrawCursor(); - _cPosition.y += DeltaY; - _RedrawCursor(); ResetDelayEOLWrap(); + if (DeltaY != 0) + { + _cPosition.y = _cPosition.y + DeltaY; + _redrawIfVisible(); + } } void Cursor::DecrementXPosition(const til::CoordType DeltaX) noexcept { - _RedrawCursor(); - _cPosition.x -= DeltaX; - _RedrawCursor(); ResetDelayEOLWrap(); + if (DeltaX != 0) + { + _cPosition.x = _cPosition.x - DeltaX; + _redrawIfVisible(); + } } void Cursor::DecrementYPosition(const til::CoordType DeltaY) noexcept { - _RedrawCursor(); - _cPosition.y -= DeltaY; - _RedrawCursor(); ResetDelayEOLWrap(); + if (DeltaY != 0) + { + _cPosition.y = _cPosition.y - DeltaY; + _redrawIfVisible(); + } } /////////////////////////////////////////////////////////////////////////////// @@ -233,78 +176,53 @@ void Cursor::DecrementYPosition(const til::CoordType DeltaY) noexcept // - OtherCursor - The cursor to copy properties from // Return Value: // - -void Cursor::CopyProperties(const Cursor& OtherCursor) noexcept +void Cursor::CopyProperties(const Cursor& other) noexcept { - // We shouldn't copy the position as it will be already rearranged by the resize operation. - //_cPosition = pOtherCursor->_cPosition; - - _fIsVisible = OtherCursor._fIsVisible; - _fIsOn = OtherCursor._fIsOn; - _fIsDouble = OtherCursor._fIsDouble; - _fBlinkingAllowed = OtherCursor._fBlinkingAllowed; - _fDelay = OtherCursor._fDelay; - _fIsConversionArea = OtherCursor._fIsConversionArea; - - // A resize operation should invalidate the delayed end of line status, so do not copy. - //_fDelayedEolWrap = OtherCursor._fDelayedEolWrap; - //_coordDelayedAt = OtherCursor._coordDelayedAt; - - _fDeferCursorRedraw = OtherCursor._fDeferCursorRedraw; - _fHaveDeferredCursorRedraw = OtherCursor._fHaveDeferredCursorRedraw; - - // Size will be handled separately in the resize operation. - //_ulSize = OtherCursor._ulSize; - _cursorType = OtherCursor._cursorType; + _cPosition = other._cPosition; + _coordDelayedAt = other._coordDelayedAt; + _ulSize = other._ulSize; + _cursorType = other._cursorType; + _isVisible = other._isVisible; + _isBlinking = other._isBlinking; + _fIsDouble = other._fIsDouble; } void Cursor::DelayEOLWrap() noexcept { _coordDelayedAt = _cPosition; - _fDelayedEolWrap = true; } void Cursor::ResetDelayEOLWrap() noexcept { - _coordDelayedAt = {}; - _fDelayedEolWrap = false; + _coordDelayedAt.reset(); } -til::point Cursor::GetDelayedAtPosition() const noexcept +const std::optional& Cursor::GetDelayEOLWrap() const noexcept { return _coordDelayedAt; } -bool Cursor::IsDelayedEOLWrap() const noexcept +CursorType Cursor::GetType() const noexcept { - return _fDelayedEolWrap; -} - -void Cursor::StartDeferDrawing() noexcept -{ - _fDeferCursorRedraw = true; + return _cursorType; } -bool Cursor::IsDeferDrawing() noexcept +void Cursor::SetType(const CursorType type) noexcept { - return _fDeferCursorRedraw; + _cursorType = type; } -void Cursor::EndDeferDrawing() noexcept +void Cursor::_redrawIfVisible() noexcept { - if (_fHaveDeferredCursorRedraw) + _mutationId++; + if (_isVisible) { - _RedrawCursorAlways(); + _parentBuffer.NotifyPaintFrame(); } - - _fDeferCursorRedraw = FALSE; } -const CursorType Cursor::GetType() const noexcept +void Cursor::_redraw() noexcept { - return _cursorType; -} - -void Cursor::SetType(const CursorType type) noexcept -{ - _cursorType = type; + _mutationId++; + _parentBuffer.NotifyPaintFrame(); } diff --git a/src/buffer/out/cursor.h b/src/buffer/out/cursor.h index d2697eeb69b..ed952c23e45 100644 --- a/src/buffer/out/cursor.h +++ b/src/buffer/out/cursor.h @@ -29,8 +29,6 @@ class Cursor final Cursor(const ULONG ulSize, TextBuffer& parentBuffer) noexcept; - ~Cursor(); - // No Copy. It will copy the timer handle. Bad news. Cursor(const Cursor&) = delete; Cursor& operator=(const Cursor&) & = delete; @@ -38,27 +36,17 @@ class Cursor final Cursor(Cursor&&) = default; Cursor& operator=(Cursor&&) & = delete; + uint64_t GetLastMutationId() const noexcept; bool IsVisible() const noexcept; - bool IsOn() const noexcept; - bool IsBlinkingAllowed() const noexcept; + bool IsBlinking() const noexcept; bool IsDouble() const noexcept; - bool IsConversionArea() const noexcept; - bool GetDelay() const noexcept; ULONG GetSize() const noexcept; til::point GetPosition() const noexcept; + CursorType GetType() const noexcept; - const CursorType GetType() const noexcept; - - void StartDeferDrawing() noexcept; - bool IsDeferDrawing() noexcept; - void EndDeferDrawing() noexcept; - - void SetIsVisible(const bool fIsVisible) noexcept; - void SetIsOn(const bool fIsOn) noexcept; - void SetBlinkingAllowed(const bool fIsOn) noexcept; + void SetIsVisible(bool enable); + void SetIsBlinking(bool enable); void SetIsDouble(const bool fIsDouble) noexcept; - void SetIsConversionArea(const bool fIsConversionArea) noexcept; - void SetDelay(const bool fDelay) noexcept; void SetSize(const ULONG ulSize) noexcept; void SetStyle(const ULONG ulSize, const CursorType type) noexcept; @@ -70,41 +58,30 @@ class Cursor final void DecrementXPosition(const til::CoordType DeltaX) noexcept; void DecrementYPosition(const til::CoordType DeltaY) noexcept; - void CopyProperties(const Cursor& OtherCursor) noexcept; + void CopyProperties(const Cursor& other) noexcept; void DelayEOLWrap() noexcept; void ResetDelayEOLWrap() noexcept; - til::point GetDelayedAtPosition() const noexcept; - bool IsDelayedEOLWrap() const noexcept; + const std::optional& GetDelayEOLWrap() const noexcept; void SetType(const CursorType type) noexcept; private: + void _redrawIfVisible() noexcept; + void _redraw() noexcept; + TextBuffer& _parentBuffer; //TODO: separate the rendering and text placement // NOTE: If you are adding a property here, go add it to CopyProperties. + uint64_t _mutationId = 0; til::point _cPosition; // current position on screen (in screen buffer coords). - - bool _fIsVisible; // whether cursor is visible (set only through the API) - bool _fIsOn; // whether blinking cursor is on or not - bool _fIsDouble; // whether the cursor size should be doubled - bool _fBlinkingAllowed; //Whether or not the cursor is allowed to blink at all. only set through VT (^[[?12h/l) - bool _fDelay; // don't blink scursor on next timer message - bool _fIsConversionArea; // is attached to a conversion area so it doesn't actually need to display the cursor. - - bool _fDelayedEolWrap; // don't wrap at EOL till the next char comes in. - til::point _coordDelayedAt; // coordinate the EOL wrap was delayed at. - - bool _fDeferCursorRedraw; // whether we should defer redrawing the cursor or not - bool _fHaveDeferredCursorRedraw; // have we been asked to redraw the cursor while it was being deferred? - + std::optional _coordDelayedAt; // coordinate the EOL wrap was delayed at. ULONG _ulSize; - - void _RedrawCursor() noexcept; - void _RedrawCursorAlways() noexcept; - - CursorType _cursorType; + CursorType _cursorType = CursorType::Legacy; + bool _isVisible = true; + bool _isBlinking = true; + bool _fIsDouble = false; // whether the cursor size should be doubled }; diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 51516913608..82a22c551bf 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1883,7 +1883,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (read < sizeof(buffer)) { // Normally the cursor should already be at the start of the line, but let's be absolutely sure it is. - if (_terminal->GetCursorPosition().x != 0) + if (_terminal->GetTextBuffer().GetCursor().GetPosition().x != 0) { _terminal->Write(L"\r\n"); } @@ -1938,31 +1938,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation TabColorChanged.raise(*this, nullptr); } - void ControlCore::BlinkAttributeTick() - { - const auto lock = _terminal->LockForWriting(); - - auto& renderSettings = _terminal->GetRenderSettings(); - renderSettings.ToggleBlinkRendition(_renderer.get()); - } - - void ControlCore::BlinkCursor() - { - const auto lock = _terminal->LockForWriting(); - _terminal->BlinkCursor(); - } - - bool ControlCore::CursorOn() const - { - return _terminal->IsCursorOn(); - } - - void ControlCore::CursorOn(const bool isCursorOn) - { - const auto lock = _terminal->LockForWriting(); - _terminal->SetCursorOn(isCursorOn); - } - void ControlCore::ResumeRendering() { // The lock must be held, because it calls into IRenderData which is shared state. @@ -2073,7 +2048,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // // As noted in GH #8573, there's plenty of edge cases with this // approach, but it's good enough to bring value to 90% of use cases. - const auto cursorPos{ _terminal->GetCursorPosition() }; // Does the current buffer line have a mark on it? const auto& marks{ _terminal->GetMarkExtents() }; @@ -2081,8 +2055,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation { const auto& last{ marks.back() }; const auto [start, end] = last.GetExtent(); - const auto bufferSize = _terminal->GetTextBuffer().GetSize(); - auto lastNonSpace = _terminal->GetTextBuffer().GetLastNonSpaceCharacter(); + const auto& buffer = _terminal->GetTextBuffer(); + const auto cursorPos = buffer.GetCursor().GetPosition(); + const auto bufferSize = buffer.GetSize(); + auto lastNonSpace = buffer.GetLastNonSpaceCharacter(); bufferSize.IncrementInBounds(lastNonSpace, true); // If the user clicked off to the right side of the prompt, we @@ -2460,7 +2436,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation { TerminalInput::OutputType out; { - const auto lock = _terminal->LockForReading(); + const auto lock = _terminal->LockForWriting(); + _renderer->AllowCursorVisibility(Render::InhibitionSource::Host, focused); out = _terminal->FocusChanged(focused); } if (out && !out->empty()) diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index d1141e0ff21..17db2145a9d 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -214,11 +214,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation #pragma endregion - void BlinkAttributeTick(); - void BlinkCursor(); - bool CursorOn() const; - void CursorOn(const bool isCursorOn); - bool IsVtMouseModeEnabled() const; bool ShouldSendAlternateScroll(const unsigned int uiButton, const int32_t delta) const; Core::Point CursorPosition() const; diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 5844c665177..62488c3aaea 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -151,7 +151,6 @@ namespace Microsoft.Terminal.Control Microsoft.Terminal.Core.Point CursorPosition { get; }; void ResumeRendering(); - void BlinkAttributeTick(); SearchResults Search(SearchRequest request); void ClearSearch(); @@ -165,9 +164,7 @@ namespace Microsoft.Terminal.Control String HoveredUriText { get; }; Windows.Foundation.IReference HoveredCell { get; }; - void BlinkCursor(); Boolean IsInReadOnlyMode { get; }; - Boolean CursorOn; void EnablePainting(); String ReadEntireBuffer(); diff --git a/src/cascadia/TerminalControl/HwndTerminal.cpp b/src/cascadia/TerminalControl/HwndTerminal.cpp index 7b2237f3fe3..58c9f34e4e3 100644 --- a/src/cascadia/TerminalControl/HwndTerminal.cpp +++ b/src/cascadia/TerminalControl/HwndTerminal.cpp @@ -63,7 +63,7 @@ RECT HwndTerminal::TsfDataProvider::GetCursorPosition() til::size fontSize; { const auto lock = _terminal->_terminal->LockForReading(); - cursorPos = _terminal->_terminal->GetCursorPosition(); // measured in terminal cells + cursorPos = _terminal->_terminal->GetTextBuffer().GetCursor().GetPosition(); // measured in terminal cells fontSize = _terminal->_actualFont.GetSize(); // measured in pixels, not DIP } POINT ptSuggestion = { @@ -706,15 +706,6 @@ void HwndTerminal::_ClearSelection() _renderer->TriggerSelection(); } -void _stdcall TerminalClearSelection(void* terminal) -try -{ - const auto publicTerminal = static_cast(terminal); - const auto lock = publicTerminal->_terminal->LockForWriting(); - publicTerminal->_ClearSelection(); -} -CATCH_LOG() - bool _stdcall TerminalIsSelectionActive(void* terminal) try { @@ -986,60 +977,52 @@ void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR font publicTerminal->Refresh(windowSize, &dimensions); } -void _stdcall TerminalBlinkCursor(void* terminal) -try +void __stdcall TerminalSetFocused(void* terminal, bool focused) { - const auto publicTerminal = static_cast(terminal); - if (!publicTerminal || !publicTerminal->_terminal) - { - return; - } - - const auto lock = publicTerminal->_terminal->LockForWriting(); - publicTerminal->_terminal->BlinkCursor(); + const auto publicTerminal = static_cast(terminal); + publicTerminal->_setFocused(focused); } -CATCH_LOG() -void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible) -try +void HwndTerminal::_setFocused(bool focused) noexcept { - const auto publicTerminal = static_cast(terminal); - if (!publicTerminal || !publicTerminal->_terminal) + if (_focused == focused) { return; } - const auto lock = publicTerminal->_terminal->LockForWriting(); - publicTerminal->_terminal->SetCursorOn(visible); -} -CATCH_LOG() -void __stdcall TerminalSetFocus(void* terminal) -{ - const auto publicTerminal = static_cast(terminal); - publicTerminal->_focused = true; - if (auto uiaEngine = publicTerminal->_uiaEngine.get()) + TerminalInput::OutputType out; { - LOG_IF_FAILED(uiaEngine->Enable()); - } - publicTerminal->_FocusTSF(); -} + const auto lock = _terminal->LockForWriting(); -void HwndTerminal::_FocusTSF() noexcept -{ - if (!_tsfHandle) - { - _tsfHandle = Microsoft::Console::TSF::Handle::Create(); - _tsfHandle.AssociateFocus(&_tsfDataProvider); - } -} + _focused = focused; -void __stdcall TerminalKillFocus(void* terminal) -{ - const auto publicTerminal = static_cast(terminal); - publicTerminal->_focused = false; - if (auto uiaEngine = publicTerminal->_uiaEngine.get()) + if (focused) + { + if (!_tsfHandle) + { + _tsfHandle = Microsoft::Console::TSF::Handle::Create(); + _tsfHandle.AssociateFocus(&_tsfDataProvider); + } + + if (const auto uiaEngine = _uiaEngine.get()) + { + LOG_IF_FAILED(uiaEngine->Enable()); + } + } + else + { + if (const auto uiaEngine = _uiaEngine.get()) + { + LOG_IF_FAILED(uiaEngine->Disable()); + } + } + + _renderer->AllowCursorVisibility(Microsoft::Console::Render::InhibitionSource::Host, focused); + out = _terminal->FocusChanged(focused); + } + if (out) { - LOG_IF_FAILED(uiaEngine->Disable()); + _WriteTextToConnection(*out); } } diff --git a/src/cascadia/TerminalControl/HwndTerminal.hpp b/src/cascadia/TerminalControl/HwndTerminal.hpp index fef4670e49f..4d417419526 100644 --- a/src/cascadia/TerminalControl/HwndTerminal.hpp +++ b/src/cascadia/TerminalControl/HwndTerminal.hpp @@ -49,7 +49,6 @@ __declspec(dllexport) HRESULT _stdcall TerminalTriggerResizeWithDimension(_In_ v __declspec(dllexport) HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ til::CoordType width, _In_ til::CoordType height, _Out_ til::size* dimensions); __declspec(dllexport) void _stdcall TerminalDpiChanged(void* terminal, int newDpi); __declspec(dllexport) void _stdcall TerminalUserScroll(void* terminal, int viewTop); -__declspec(dllexport) void _stdcall TerminalClearSelection(void* terminal); __declspec(dllexport) const wchar_t* _stdcall TerminalGetSelection(void* terminal); __declspec(dllexport) bool _stdcall TerminalIsSelectionActive(void* terminal); __declspec(dllexport) void _stdcall DestroyTerminal(void* terminal); @@ -57,10 +56,7 @@ __declspec(dllexport) void _stdcall TerminalSetTheme(void* terminal, TerminalThe __declspec(dllexport) void _stdcall TerminalRegisterWriteCallback(void* terminal, const void __stdcall callback(wchar_t*)); __declspec(dllexport) void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, WORD flags, bool keyDown); __declspec(dllexport) void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD flags, WORD scanCode); -__declspec(dllexport) void _stdcall TerminalBlinkCursor(void* terminal); -__declspec(dllexport) void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible); -__declspec(dllexport) void _stdcall TerminalSetFocus(void* terminal); -__declspec(dllexport) void _stdcall TerminalKillFocus(void* terminal); +__declspec(dllexport) void _stdcall TerminalSetFocused(void* terminal, bool focused); }; struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo @@ -91,9 +87,9 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo TsfDataProvider(HwndTerminal* t) : _terminal(t) {} virtual ~TsfDataProvider() = default; - STDMETHODIMP TsfDataProvider::QueryInterface(REFIID, void**) noexcept override; - ULONG STDMETHODCALLTYPE TsfDataProvider::AddRef() noexcept override; - ULONG STDMETHODCALLTYPE TsfDataProvider::Release() noexcept override; + STDMETHODIMP QueryInterface(REFIID, void**) noexcept override; + ULONG STDMETHODCALLTYPE AddRef() noexcept override; + ULONG STDMETHODCALLTYPE Release() noexcept override; HWND GetHwnd() override; RECT GetViewport() override; RECT GetCursorPosition() override; @@ -132,16 +128,12 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo friend HRESULT _stdcall TerminalCalculateResize(_In_ void* terminal, _In_ til::CoordType width, _In_ til::CoordType height, _Out_ til::size* dimensions); friend void _stdcall TerminalDpiChanged(void* terminal, int newDpi); friend void _stdcall TerminalUserScroll(void* terminal, int viewTop); - friend void _stdcall TerminalClearSelection(void* terminal); friend const wchar_t* _stdcall TerminalGetSelection(void* terminal); friend bool _stdcall TerminalIsSelectionActive(void* terminal); friend void _stdcall TerminalSendKeyEvent(void* terminal, WORD vkey, WORD scanCode, WORD flags, bool keyDown); friend void _stdcall TerminalSendCharEvent(void* terminal, wchar_t ch, WORD scanCode, WORD flags); friend void _stdcall TerminalSetTheme(void* terminal, TerminalTheme theme, LPCWSTR fontFamily, til::CoordType fontSize, int newDpi); - friend void _stdcall TerminalBlinkCursor(void* terminal); - friend void _stdcall TerminalSetCursorVisible(void* terminal, const bool visible); - friend void _stdcall TerminalSetFocus(void* terminal); - friend void _stdcall TerminalKillFocus(void* terminal); + friend void _stdcall TerminalSetFocused(void* terminal, bool focused); void _UpdateFont(int newDpi); void _WriteTextToConnection(const std::wstring_view text) noexcept; @@ -149,7 +141,7 @@ struct HwndTerminal : ::Microsoft::Console::Types::IControlAccessibilityInfo HRESULT _CopyToSystemClipboard(wil::zstring_view stringToCopy, LPCWSTR lpszFormat) const; void _PasteTextFromClipboard() noexcept; - void _FocusTSF() noexcept; + void _setFocused(bool focused) noexcept; const unsigned int _NumberOfClicks(til::point clickPos, std::chrono::steady_clock::time_point clickTime) noexcept; HRESULT _StartSelection(LPARAM lParam) noexcept; diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 9a330526d24..83743fd3f49 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -1401,51 +1401,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation ScrollBar().ViewportSize(bufferHeight); ScrollBar().LargeChange(bufferHeight); // scroll one "screenful" at a time when the scroll bar is clicked - // Set up blinking cursor - int blinkTime = GetCaretBlinkTime(); - if (blinkTime != INFINITE) - { - // Create a timer - _cursorTimer.Interval(std::chrono::milliseconds(blinkTime)); - _cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick }); - // As of GH#6586, don't start the cursor timer immediately, and - // don't show the cursor initially. We'll show the cursor and start - // the timer when the control is first focused. - // - // As of GH#11411, turn on the cursor if we've already been marked - // as focused. We suspect that it's possible for the Focused event - // to fire before the LayoutUpdated. In that case, the - // _GotFocusHandler would mark us _focused, but find that a - // _cursorTimer doesn't exist, and it would never turn on the - // cursor. To mitigate, we'll initialize the cursor's 'on' state - // with `_focused` here. - _core.CursorOn(_focused || _displayCursorWhileBlurred()); - if (_displayCursorWhileBlurred()) - { - _cursorTimer.Start(); - } - } - else - { - _cursorTimer.Destroy(); - } - - // Set up blinking attributes - auto animationsEnabled = TRUE; - SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0); - if (animationsEnabled && blinkTime != INFINITE) - { - // Create a timer - _blinkTimer.Interval(std::chrono::milliseconds(blinkTime)); - _blinkTimer.Tick({ get_weak(), &TermControl::_BlinkTimerTick }); - _blinkTimer.Start(); - } - else - { - // The user has disabled blinking - _blinkTimer.Destroy(); - } - // Now that the renderer is set up, update the appearance for initialization _UpdateAppearanceFromUIThread(_core.FocusedAppearance()); @@ -1938,14 +1893,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation get_self(_automationPeer)->RecordKeyEvent(vkey); } - if (_cursorTimer) - { - // Manually show the cursor when a key is pressed. Restarting - // the timer prevents flickering. - _core.CursorOn(_core.SelectionMode() != SelectionInteractionMode::Mark); - _cursorTimer.Start(); - } - return handled; } @@ -2403,17 +2350,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation { return; } - if (_cursorTimer) - { - // When the terminal focuses, show the cursor immediately - _core.CursorOn(_core.SelectionMode() != SelectionInteractionMode::Mark); - _cursorTimer.Start(); - } - - if (_blinkTimer) - { - _blinkTimer.Start(); - } // Only update the appearance here if an unfocused config exists - if an // unfocused config does not exist then we never would have switched @@ -2450,17 +2386,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _interactivity.LostFocus(); } - if (_cursorTimer && !_displayCursorWhileBlurred()) - { - _cursorTimer.Stop(); - _core.CursorOn(false); - } - - if (_blinkTimer) - { - _blinkTimer.Stop(); - } - // Check if there is an unfocused config we should set the appearance to // upon losing focus if (_core.HasUnfocusedAppearance()) @@ -2528,34 +2453,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.ScaleChanged(scaleX); } - // Method Description: - // - Toggle the cursor on and off when called by the cursor blink timer. - // Arguments: - // - sender: not used - // - e: not used - void TermControl::_CursorTimerTick(const Windows::Foundation::IInspectable& /* sender */, - const Windows::Foundation::IInspectable& /* e */) - { - if (!_IsClosing()) - { - _core.BlinkCursor(); - } - } - - // Method Description: - // - Toggle the blinking rendition state when called by the blink timer. - // Arguments: - // - sender: not used - // - e: not used - void TermControl::_BlinkTimerTick(const Windows::Foundation::IInspectable& /* sender */, - const Windows::Foundation::IInspectable& /* e */) - { - if (!_IsClosing()) - { - _core.BlinkAttributeTick(); - } - } - // Method Description: // - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging. // Arguments: @@ -2724,8 +2621,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation // while the thread is supposed to be idle. Stop these timers avoids this. _autoScrollTimer.Stop(); _bellLightTimer.Stop(); - _cursorTimer.Stop(); - _blinkTimer.Stop(); // This is absolutely crucial, as the TSF code tries to hold a strong reference to _tsfDataProvider, // but right now _tsfDataProvider implements IUnknown as a no-op. This ensures that TSF stops referencing us. @@ -4181,31 +4076,5 @@ namespace winrt::Microsoft::Terminal::Control::implementation void TermControl::CursorVisibility(Control::CursorDisplayState cursorVisibility) { _cursorVisibility = cursorVisibility; - if (!_initializedTerminal) - { - return; - } - - if (_displayCursorWhileBlurred()) - { - // If we should be ALWAYS displaying the cursor, turn it on and start blinking. - _core.CursorOn(true); - if (_cursorTimer) - { - _cursorTimer.Start(); - } - } - else - { - // Otherwise, if we're unfocused, then turn the cursor off and stop - // blinking. (if we're focused, then we're already doing the right - // thing) - const auto focused = FocusState() != FocusState::Unfocused; - if (!focused && _cursorTimer) - { - _cursorTimer.Stop(); - } - _core.CursorOn(focused); - } } } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index 1342d4215a0..d9f15b9a3ac 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -313,9 +313,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation winrt::Windows::UI::Composition::ScalarKeyFrameAnimation _bellDarkAnimation{ nullptr }; SafeDispatcherTimer _bellLightTimer; - SafeDispatcherTimer _cursorTimer; - SafeDispatcherTimer _blinkTimer; - winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker; winrt::hstring _restorePath; bool _showMarksInScrollbar{ false }; @@ -387,8 +384,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation safe_void_coroutine _HyperlinkHandler(Windows::Foundation::IInspectable sender, Control::OpenHyperlinkEventArgs e); - void _CursorTimerTick(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e); - void _BlinkTimerTick(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e); void _BellLightOff(const Windows::Foundation::IInspectable& sender, const Windows::Foundation::IInspectable& e); void _SetEndSelectionPointAtCursor(const Windows::Foundation::Point& cursorPosition); diff --git a/src/cascadia/TerminalControl/dll/Microsoft.Terminal.Control.def b/src/cascadia/TerminalControl/dll/Microsoft.Terminal.Control.def index 8500c457d68..c02c0721ab0 100644 --- a/src/cascadia/TerminalControl/dll/Microsoft.Terminal.Control.def +++ b/src/cascadia/TerminalControl/dll/Microsoft.Terminal.Control.def @@ -6,20 +6,16 @@ EXPORTS ; Flat C ABI CreateTerminal DestroyTerminal - TerminalBlinkCursor TerminalCalculateResize - TerminalClearSelection TerminalDpiChanged TerminalGetSelection TerminalIsSelectionActive - TerminalKillFocus TerminalRegisterScrollCallback TerminalRegisterWriteCallback TerminalSendCharEvent TerminalSendKeyEvent TerminalSendOutput - TerminalSetCursorVisible - TerminalSetFocus + TerminalSetFocused TerminalSetTheme TerminalTriggerResize TerminalTriggerResizeWithDimension diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 4c5d6eaef18..563e54bb992 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -784,6 +784,19 @@ TerminalInput::OutputType Terminal::SendCharEvent(const wchar_t ch, const WORD s // - none TerminalInput::OutputType Terminal::FocusChanged(const bool focused) { + if (_focused == focused) + { + return {}; + } + + _focused = focused; + + // Recalculate the IRenderData::GetBlinkInterval() on the next call. + if (focused) + { + _cursorBlinkInterval.reset(); + } + return _getTerminalInput().HandleFocus(focused); } @@ -1182,31 +1195,6 @@ void Terminal::SetPlayMidiNoteCallback(std::function GetPatternId(const til::point location) const override; + bool IsGridLineDrawingAllowed() noexcept override; + std::wstring GetHyperlinkUri(uint16_t id) const override; + std::wstring GetHyperlinkCustomId(uint16_t id) const override; + std::vector GetPatternId(const til::point location) const override; std::pair GetAttributeColors(const TextAttribute& attr) const noexcept override; std::span GetSelectionSpans() const noexcept override; std::span GetSearchHighlights() const noexcept override; const til::point_span* GetSearchHighlightFocused() const noexcept override; - const bool IsSelectionActive() const noexcept override; - const bool IsBlockSelection() const noexcept override; + bool IsSelectionActive() const noexcept override; + bool IsBlockSelection() const noexcept override; void ClearSelection() override; void SelectNewRegion(const til::point coordStart, const til::point coordEnd) override; - const til::point GetSelectionAnchor() const noexcept override; - const til::point GetSelectionEnd() const noexcept override; - const std::wstring_view GetConsoleTitle() const noexcept override; - const bool IsUiaDataInitialized() const noexcept override; + til::point GetSelectionAnchor() const noexcept override; + til::point GetSelectionEnd() const noexcept override; + std::wstring_view GetConsoleTitle() const noexcept override; + bool IsUiaDataInitialized() const noexcept override; #pragma endregion void SetWriteInputCallback(std::function pfn) noexcept; @@ -240,9 +235,6 @@ class Microsoft::Terminal::Core::Terminal final : void SetSearchHighlightFocused(size_t focusedIdx) noexcept; void ScrollToSearchHighlight(til::CoordType searchScrollOffset); - void BlinkCursor() noexcept; - void SetCursorOn(const bool isOn) noexcept; - void UpdatePatternsUnderLock(); const std::optional GetTabColor() const; @@ -316,7 +308,7 @@ class Microsoft::Terminal::Core::Terminal final : UpdateSelectionParams ConvertKeyEventToUpdateSelectionParams(const ControlKeyStates mods, const WORD vkey) const noexcept; til::point SelectionStartForRendering() const; til::point SelectionEndForRendering() const; - const SelectionEndpoint SelectionEndpointTarget() const noexcept; + SelectionEndpoint SelectionEndpointTarget() const noexcept; TextCopyData RetrieveSelectedTextFromBuffer(const bool singleLine, const bool withControlSequences = false, const bool html = false, const bool rtf = false) const; #pragma endregion @@ -363,9 +355,11 @@ class Microsoft::Terminal::Core::Terminal final : mutable til::generation_t _lastSelectionGeneration{}; CursorType _defaultCursorShape = CursorType::Legacy; + std::optional _cursorBlinkInterval; til::enumset _systemMode{ Mode::AutoWrap }; + bool _focused = false; bool _snapOnInput = true; bool _altGrAliasing = true; bool _suppressApplicationTitle = false; diff --git a/src/cascadia/TerminalCore/TerminalApi.cpp b/src/cascadia/TerminalCore/TerminalApi.cpp index 9a4fa85efc9..e2c7a818144 100644 --- a/src/cascadia/TerminalCore/TerminalApi.cpp +++ b/src/cascadia/TerminalCore/TerminalApi.cpp @@ -249,7 +249,7 @@ void Terminal::UseAlternateScreenBuffer(const TextAttribute& attrs) auto& tgtCursor = _altBuffer->GetCursor(); tgtCursor.SetStyle(myCursor.GetSize(), myCursor.GetType()); tgtCursor.SetIsVisible(myCursor.IsVisible()); - tgtCursor.SetBlinkingAllowed(myCursor.IsBlinkingAllowed()); + tgtCursor.SetIsBlinking(myCursor.IsBlinking()); // The new position should match the viewport-relative position of the main buffer. auto tgtCursorPos = myCursor.GetPosition(); @@ -307,7 +307,7 @@ void Terminal::UseMainScreenBuffer() mainCursor.SetStyle(altCursor.GetSize(), altCursor.GetType()); mainCursor.SetIsVisible(altCursor.IsVisible()); - mainCursor.SetBlinkingAllowed(altCursor.IsBlinkingAllowed()); + mainCursor.SetIsBlinking(altCursor.IsBlinking()); auto tgtCursorPos = altCursor.GetPosition(); tgtCursorPos.y += _mutableViewport.Top(); @@ -359,7 +359,7 @@ void Terminal::SearchMissingCommand(const std::wstring_view command) { if (_pfnSearchMissingCommand) { - const auto bufferRow = GetCursorPosition().y; + const auto bufferRow = _activeBuffer().GetCursor().GetPosition().y; _pfnSearchMissingCommand(command, bufferRow); } } diff --git a/src/cascadia/TerminalCore/TerminalSelection.cpp b/src/cascadia/TerminalCore/TerminalSelection.cpp index f0019d66143..11286c42062 100644 --- a/src/cascadia/TerminalCore/TerminalSelection.cpp +++ b/src/cascadia/TerminalCore/TerminalSelection.cpp @@ -69,7 +69,7 @@ std::vector Terminal::_GetSelectionSpans() const noexcept // - None // Return Value: // - None -const til::point Terminal::GetSelectionAnchor() const noexcept +til::point Terminal::GetSelectionAnchor() const noexcept { _assertLocked(); return _selection->start; @@ -81,7 +81,7 @@ const til::point Terminal::GetSelectionAnchor() const noexcept // - None // Return Value: // - None -const til::point Terminal::GetSelectionEnd() const noexcept +til::point Terminal::GetSelectionEnd() const noexcept { _assertLocked(); return _selection->end; @@ -139,7 +139,7 @@ til::point Terminal::SelectionEndForRendering() const return til::point{ pos }; } -const Terminal::SelectionEndpoint Terminal::SelectionEndpointTarget() const noexcept +Terminal::SelectionEndpoint Terminal::SelectionEndpointTarget() const noexcept { return _selectionEndpoint; } @@ -148,13 +148,13 @@ const Terminal::SelectionEndpoint Terminal::SelectionEndpointTarget() const noex // - Checks if selection is active // Return Value: // - bool representing if selection is active. Used to decide copy/paste on right click -const bool Terminal::IsSelectionActive() const noexcept +bool Terminal::IsSelectionActive() const noexcept { _assertLocked(); return _selection->active; } -const bool Terminal::IsBlockSelection() const noexcept +bool Terminal::IsBlockSelection() const noexcept { _assertLocked(); return _selection->blockSelection; @@ -362,7 +362,6 @@ void Terminal::ToggleMarkMode() { // Enter Mark Mode // NOTE: directly set cursor state. We already should have locked before calling this function. - _activeBuffer().GetCursor().SetIsOn(false); if (!IsSelectionActive()) { // No selection --> start one at the cursor diff --git a/src/cascadia/TerminalCore/terminalrenderdata.cpp b/src/cascadia/TerminalCore/terminalrenderdata.cpp index d7bca1077cc..f2a9087a901 100644 --- a/src/cascadia/TerminalCore/terminalrenderdata.cpp +++ b/src/cascadia/TerminalCore/terminalrenderdata.cpp @@ -3,7 +3,6 @@ #include "pch.h" #include "Terminal.hpp" -#include using namespace Microsoft::Terminal::Core; using namespace Microsoft::Console::Types; @@ -39,22 +38,17 @@ void Terminal::SetFontInfo(const FontInfo& fontInfo) _fontInfo = fontInfo; } -til::point Terminal::GetCursorPosition() const noexcept +TimerDuration Terminal::GetBlinkInterval() noexcept { - const auto& cursor = _activeBuffer().GetCursor(); - return cursor.GetPosition(); -} - -bool Terminal::IsCursorVisible() const noexcept -{ - const auto& cursor = _activeBuffer().GetCursor(); - return cursor.IsVisible(); -} - -bool Terminal::IsCursorOn() const noexcept -{ - const auto& cursor = _activeBuffer().GetCursor(); - return cursor.IsOn(); + if (!_cursorBlinkInterval) + { + const auto enabled = GetSystemMetrics(SM_CARETBLINKINGENABLED); + const auto interval = GetCaretBlinkTime(); + // >10s --> no blinking. The limit is arbitrary, because technically the valid range + // on Windows is 200-1200ms. GetCaretBlinkTime() returns INFINITE for no blinking, 0 for errors. + _cursorBlinkInterval = enabled && interval <= 10000 ? std ::chrono::milliseconds(interval) : TimerDuration::max(); + } + return *_cursorBlinkInterval; } ULONG Terminal::GetCursorPixelWidth() const noexcept @@ -62,34 +56,17 @@ ULONG Terminal::GetCursorPixelWidth() const noexcept return 1; } -ULONG Terminal::GetCursorHeight() const noexcept -{ - return _activeBuffer().GetCursor().GetSize(); -} - -CursorType Terminal::GetCursorStyle() const noexcept -{ - return _activeBuffer().GetCursor().GetType(); -} - -bool Terminal::IsCursorDoubleWidth() const -{ - const auto& buffer = _activeBuffer(); - const auto position = buffer.GetCursor().GetPosition(); - return buffer.GetRowByOffset(position.y).DbcsAttrAt(position.x) != DbcsAttribute::Single; -} - -const bool Terminal::IsGridLineDrawingAllowed() noexcept +bool Terminal::IsGridLineDrawingAllowed() noexcept { return true; } -const std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkUri(uint16_t id) const +std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkUri(uint16_t id) const { return _activeBuffer().GetHyperlinkUriFromId(id); } -const std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkCustomId(uint16_t id) const +std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkCustomId(uint16_t id) const { return _activeBuffer().GetCustomIdFromId(id); } @@ -100,7 +77,7 @@ const std::wstring Microsoft::Terminal::Core::Terminal::GetHyperlinkCustomId(uin // - The location // Return value: // - The pattern IDs of the location -const std::vector Terminal::GetPatternId(const til::point location) const +std::vector Terminal::GetPatternId(const til::point location) const { _assertLocked(); @@ -218,7 +195,7 @@ void Terminal::SelectNewRegion(const til::point coordStart, const til::point coo _activeBuffer().TriggerSelection(); } -const std::wstring_view Terminal::GetConsoleTitle() const noexcept +std::wstring_view Terminal::GetConsoleTitle() const noexcept { _assertLocked(); if (_title.has_value()) @@ -246,7 +223,7 @@ void Terminal::UnlockConsole() noexcept _readWriteLock.unlock(); } -const bool Terminal::IsUiaDataInitialized() const noexcept +bool Terminal::IsUiaDataInitialized() const noexcept { // GH#11135: Windows Terminal needs to create and return an automation peer // when a screen reader requests it. However, the terminal might not be fully diff --git a/src/cascadia/WpfTerminalControl/NativeMethods.cs b/src/cascadia/WpfTerminalControl/NativeMethods.cs index 96898df5dc7..fdd42a1450a 100644 --- a/src/cascadia/WpfTerminalControl/NativeMethods.cs +++ b/src/cascadia/WpfTerminalControl/NativeMethods.cs @@ -92,14 +92,6 @@ public enum WindowMessage : int WM_MOUSEWHEEL = 0x020A, } - public enum VirtualKey : ushort - { - /// - /// ALT key - /// - VK_MENU = 0x12, - } - [Flags] public enum SetWindowPosFlags : uint { @@ -206,9 +198,6 @@ public enum SetWindowPosFlags : uint [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalUserScroll(IntPtr terminal, int viewTop); - [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] - public static extern void TerminalClearSelection(IntPtr terminal); - [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] [return: MarshalAs(UnmanagedType.LPWStr)] public static extern string TerminalGetSelection(IntPtr terminal); @@ -229,30 +218,12 @@ public enum SetWindowPosFlags : uint [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] public static extern void TerminalSetTheme(IntPtr terminal, [MarshalAs(UnmanagedType.Struct)] TerminalTheme theme, string fontFamily, short fontSize, int newDpi); - [DllImport("Microsoft.Terminal.Control.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = true)] - public static extern void TerminalBlinkCursor(IntPtr terminal); - - [DllImport("Microsoft.Terminal.Control.dll", CallingConvention = CallingConvention.StdCall, PreserveSig = true)] - public static extern void TerminalSetCursorVisible(IntPtr terminal, bool visible); - [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] - public static extern void TerminalSetFocus(IntPtr terminal); - - [DllImport("Microsoft.Terminal.Control.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall, PreserveSig = true)] - public static extern void TerminalKillFocus(IntPtr terminal); + public static extern void TerminalSetFocused(IntPtr terminal, bool focused); [DllImport("user32.dll", SetLastError = true)] public static extern IntPtr SetFocus(IntPtr hWnd); - [DllImport("user32.dll", SetLastError = true)] - public static extern IntPtr GetFocus(); - - [DllImport("user32.dll", SetLastError = true)] - public static extern short GetKeyState(int keyCode); - - [DllImport("user32.dll", SetLastError = true)] - public static extern uint GetCaretBlinkTime(); - [StructLayout(LayoutKind.Sequential)] public struct WINDOWPOS { diff --git a/src/cascadia/WpfTerminalControl/TerminalContainer.cs b/src/cascadia/WpfTerminalControl/TerminalContainer.cs index 967d5b4f7d1..547338f9098 100644 --- a/src/cascadia/WpfTerminalControl/TerminalContainer.cs +++ b/src/cascadia/WpfTerminalControl/TerminalContainer.cs @@ -24,7 +24,6 @@ public class TerminalContainer : HwndHost private ITerminalConnection connection; private IntPtr hwnd; private IntPtr terminal; - private DispatcherTimer blinkTimer; private NativeMethods.ScrollCallback scrollCallback; private NativeMethods.WriteCallback writeCallback; @@ -36,23 +35,6 @@ public TerminalContainer() this.MessageHook += this.TerminalContainer_MessageHook; this.GotFocus += this.TerminalContainer_GotFocus; this.Focusable = true; - - var blinkTime = NativeMethods.GetCaretBlinkTime(); - - if (blinkTime == uint.MaxValue) - { - return; - } - - this.blinkTimer = new DispatcherTimer(); - this.blinkTimer.Interval = TimeSpan.FromMilliseconds(blinkTime); - this.blinkTimer.Tick += (_, __) => - { - if (this.terminal != IntPtr.Zero) - { - NativeMethods.TerminalBlinkCursor(this.terminal); - } - }; } /// @@ -314,15 +296,6 @@ protected override HandleRef BuildWindowCore(HandleRef hwndParent) NativeMethods.TerminalDpiChanged(this.terminal, (int)dpiScale.PixelsPerInchX); } - if (NativeMethods.GetFocus() == this.hwnd) - { - this.blinkTimer?.Start(); - } - else - { - NativeMethods.TerminalSetCursorVisible(this.terminal, false); - } - return new HandleRef(this, this.hwnd); } @@ -360,13 +333,10 @@ private IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam switch ((NativeMethods.WindowMessage)msg) { case NativeMethods.WindowMessage.WM_SETFOCUS: - NativeMethods.TerminalSetFocus(this.terminal); - this.blinkTimer?.Start(); + NativeMethods.TerminalSetFocused(this.terminal, true); break; case NativeMethods.WindowMessage.WM_KILLFOCUS: - NativeMethods.TerminalKillFocus(this.terminal); - this.blinkTimer?.Stop(); - NativeMethods.TerminalSetCursorVisible(this.terminal, false); + NativeMethods.TerminalSetFocused(this.terminal, false); break; case NativeMethods.WindowMessage.WM_MOUSEACTIVATE: this.Focus(); @@ -375,12 +345,8 @@ private IntPtr TerminalContainer_MessageHook(IntPtr hwnd, int msg, IntPtr wParam case NativeMethods.WindowMessage.WM_SYSKEYDOWN: // fallthrough case NativeMethods.WindowMessage.WM_KEYDOWN: { - // WM_KEYDOWN lParam layout documentation: https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-keydown - NativeMethods.TerminalSetCursorVisible(this.terminal, true); - UnpackKeyMessage(wParam, lParam, out ushort vkey, out ushort scanCode, out ushort flags); NativeMethods.TerminalSendKeyEvent(this.terminal, vkey, scanCode, flags, true); - this.blinkTimer?.Start(); break; } diff --git a/src/host/CursorBlinker.cpp b/src/host/CursorBlinker.cpp deleted file mode 100644 index 992bcec9d85..00000000000 --- a/src/host/CursorBlinker.cpp +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "../host/scrolling.hpp" -#include "../interactivity/inc/ServiceLocator.hpp" -#pragma hdrstop - -using namespace Microsoft::Console; -using namespace Microsoft::Console::Interactivity; -using namespace Microsoft::Console::Render; - -static void CALLBACK CursorTimerRoutineWrapper(_Inout_ PTP_CALLBACK_INSTANCE /*Instance*/, _Inout_opt_ PVOID /*Context*/, _Inout_ PTP_TIMER /*Timer*/) -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - // There's a slight race condition here. - // CreateThreadpoolTimer callbacks may be scheduled even after they were canceled. - // But I'm not too concerned that this will lead to issues at the time of writing, - // as CursorBlinker is allocated as a static variable through the Globals class. - // It'd be nice to fix this, but realistically it'll likely not lead to issues. - gci.LockConsole(); - gci.GetCursorBlinker().TimerRoutine(gci.GetActiveOutputBuffer()); - gci.UnlockConsole(); -} - -CursorBlinker::CursorBlinker() : - _timer(THROW_LAST_ERROR_IF_NULL(CreateThreadpoolTimer(&CursorTimerRoutineWrapper, nullptr, nullptr))), - _uCaretBlinkTime(INFINITE) // default to no blink -{ -} - -CursorBlinker::~CursorBlinker() -{ - KillCaretTimer(); -} - -void CursorBlinker::UpdateSystemMetrics() noexcept -{ - // This can be -1 in a TS session - _uCaretBlinkTime = ServiceLocator::LocateSystemConfigurationProvider()->GetCaretBlinkTime(); - - // If animations are disabled, or the blink rate is infinite, blinking is not allowed. - auto animationsEnabled = TRUE; - SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0); - auto& renderSettings = ServiceLocator::LocateGlobals().getConsoleInformation().GetRenderSettings(); - renderSettings.SetRenderMode(RenderSettings::Mode::BlinkAllowed, animationsEnabled && _uCaretBlinkTime != INFINITE); -} - -void CursorBlinker::SettingsChanged() noexcept -{ - const auto dwCaretBlinkTime = ServiceLocator::LocateSystemConfigurationProvider()->GetCaretBlinkTime(); - - if (dwCaretBlinkTime != _uCaretBlinkTime) - { - KillCaretTimer(); - _uCaretBlinkTime = dwCaretBlinkTime; - SetCaretTimer(); - } -} - -void CursorBlinker::FocusEnd() const noexcept -{ - KillCaretTimer(); -} - -void CursorBlinker::FocusStart() const noexcept -{ - SetCaretTimer(); -} - -// Routine Description: -// - This routine is called when the timer in the console with the focus goes off. -// It blinks the cursor and also toggles the rendition of any blinking attributes. -// Arguments: -// - ScreenInfo - reference to screen info structure. -// Return Value: -// - -void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo) const noexcept -{ - auto& buffer = ScreenInfo.GetTextBuffer(); - auto& cursor = buffer.GetCursor(); - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - - if (!WI_IsFlagSet(gci.Flags, CONSOLE_HAS_FOCUS)) - { - goto DoScroll; - } - - // If the DelayCursor flag has been set, wait one more tick before toggle. - // This is used to guarantee the cursor is on for a finite period of time - // after a move and off for a finite period of time after a WriteString. - if (cursor.GetDelay()) - { - cursor.SetDelay(false); - goto DoBlinkingRenditionAndScroll; - } - - // Don't blink the cursor for remote sessions. - if ((!ServiceLocator::LocateSystemConfigurationProvider()->IsCaretBlinkingEnabled() || - _uCaretBlinkTime == -1 || - (!cursor.IsBlinkingAllowed())) && - cursor.IsOn()) - { - goto DoBlinkingRenditionAndScroll; - } - - // Blink only if the cursor isn't turned off via the API - if (cursor.IsVisible()) - { - cursor.SetIsOn(!cursor.IsOn()); - } - -DoBlinkingRenditionAndScroll: - gci.GetRenderSettings().ToggleBlinkRendition(buffer.GetRenderer()); - -DoScroll: - Scrolling::s_ScrollIfNecessary(ScreenInfo); -} - -// Routine Description: -// - If guCaretBlinkTime is -1, we don't want to blink the caret. However, we -// need to make sure it gets drawn, so we'll set a short timer. When that -// goes off, we'll hit CursorTimerRoutine, and it'll do the right thing if -// guCaretBlinkTime is -1. -void CursorBlinker::SetCaretTimer() const noexcept -{ - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - if (gci.IsInVtIoMode()) - { - return; - } - - using filetime_duration = std::chrono::duration>; - static constexpr DWORD dwDefTimeout = 0x212; - - const auto periodInMS = _uCaretBlinkTime == -1 ? dwDefTimeout : _uCaretBlinkTime; - // The FILETIME struct measures time in 100ns steps. 10000 thus equals 1ms. - auto periodInFiletime = -static_cast(periodInMS) * 10000; - SetThreadpoolTimer(_timer.get(), reinterpret_cast(&periodInFiletime), periodInMS, 0); -} - -void CursorBlinker::KillCaretTimer() const noexcept -{ - SetThreadpoolTimer(_timer.get(), nullptr, 0, 0); -} diff --git a/src/host/CursorBlinker.hpp b/src/host/CursorBlinker.hpp deleted file mode 100644 index ec4443951e5..00000000000 --- a/src/host/CursorBlinker.hpp +++ /dev/null @@ -1,37 +0,0 @@ -/* -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- CursorBlinker.hpp -Abstract: -- Encapsulates all of the behavior needed to blink the cursor, and update the - blink rate to account for different system settings. - -Author(s): -- Mike Griese (migrie) Nov 2018 -*/ - -namespace Microsoft::Console -{ - class CursorBlinker final - { - public: - CursorBlinker(); - ~CursorBlinker(); - - void FocusStart() const noexcept; - void FocusEnd() const noexcept; - - void UpdateSystemMetrics() noexcept; - void SettingsChanged() noexcept; - void TimerRoutine(SCREEN_INFORMATION& ScreenInfo) const noexcept; - - private: - void SetCaretTimer() const noexcept; - void KillCaretTimer() const noexcept; - - wil::unique_threadpool_timer_nowait _timer; - UINT _uCaretBlinkTime; - }; -} diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index bb558470cc5..93c5875047d 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -131,7 +131,7 @@ static void AdjustCursorPosition(SCREEN_INFORMATION& screenInfo, _In_ til::point coordCursor.y = bufferSize.height - 1; } - LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor, false)); + LOG_IF_FAILED(screenInfo.SetCursorPosition(coordCursor)); } // As the name implies, this writes text without processing its control characters. @@ -191,10 +191,9 @@ void WriteCharsLegacy(SCREEN_INFORMATION& screenInfo, const std::wstring_view& t // If we enter this if condition, then someone wrote text in VT mode and now switched to non-VT mode. // Since the Console APIs don't support delayed EOL wrapping, we need to first put the cursor back // to a position that the Console APIs expect (= not delayed). - if (cursor.IsDelayedEOLWrap() && wrapAtEOL) + if (const auto delayed = cursor.GetDelayEOLWrap(); delayed && wrapAtEOL) { auto pos = cursor.GetPosition(); - const auto delayed = cursor.GetDelayedAtPosition(); cursor.ResetDelayEOLWrap(); if (delayed == pos) { diff --git a/src/host/consoleInformation.cpp b/src/host/consoleInformation.cpp index 7146bee4382..7da39ecd347 100644 --- a/src/host/consoleInformation.cpp +++ b/src/host/consoleInformation.cpp @@ -355,17 +355,6 @@ const std::wstring_view CONSOLE_INFORMATION::GetLinkTitle() const noexcept return _LinkTitle; } -// Method Description: -// - return a reference to the console's cursor blinker. -// Arguments: -// - -// Return Value: -// - a reference to the console's cursor blinker. -Microsoft::Console::CursorBlinker& CONSOLE_INFORMATION::GetCursorBlinker() noexcept -{ - return _blinker; -} - // Method Description: // - Returns the MIDI audio instance. // Arguments: diff --git a/src/host/ft_host/API_InputTests.cpp b/src/host/ft_host/API_InputTests.cpp index cbdb5815342..3bb9f39b66d 100644 --- a/src/host/ft_host/API_InputTests.cpp +++ b/src/host/ft_host/API_InputTests.cpp @@ -94,7 +94,7 @@ void InputTests::TestGetMouseButtonsValid() } else { - dwButtonsExpected = Microsoft::Console::Interactivity::OneCore::SystemConfigurationProvider::s_DefaultNumberOfMouseButtons; + dwButtonsExpected = 3; } VERIFY_ARE_EQUAL(dwButtonsExpected, nMouseButtons); diff --git a/src/host/getset.cpp b/src/host/getset.cpp index 3568037bc50..601dea0cc17 100644 --- a/src/host/getset.cpp +++ b/src/host/getset.cpp @@ -749,7 +749,7 @@ void ApiRoutines::GetLargestConsoleWindowSizeImpl(const SCREEN_INFORMATION& cont writer.Submit(); } - RETURN_IF_NTSTATUS_FAILED(buffer.SetCursorPosition(position, true)); + RETURN_IF_NTSTATUS_FAILED(buffer.SetCursorPosition(position)); // Attempt to "snap" the viewport to the cursor position. If the cursor // is not in the current viewport, we'll try and move the viewport so diff --git a/src/host/host-common.vcxitems b/src/host/host-common.vcxitems index 1b60faafd99..c5ae9909a83 100644 --- a/src/host/host-common.vcxitems +++ b/src/host/host-common.vcxitems @@ -4,7 +4,6 @@ - @@ -59,7 +58,6 @@ - diff --git a/src/host/lib/hostlib.vcxproj.filters b/src/host/lib/hostlib.vcxproj.filters index 255188cae87..f149308c8a9 100644 --- a/src/host/lib/hostlib.vcxproj.filters +++ b/src/host/lib/hostlib.vcxproj.filters @@ -144,12 +144,6 @@ Source Files - - Source Files - - - Source Files - @@ -284,9 +278,6 @@ Header Files - - Header Files - Header Files @@ -298,4 +289,4 @@ - \ No newline at end of file + diff --git a/src/host/output.cpp b/src/host/output.cpp index 4f5c2c94c73..e0dd534ef01 100644 --- a/src/host/output.cpp +++ b/src/host/output.cpp @@ -435,21 +435,6 @@ void SetActiveScreenBuffer(SCREEN_INFORMATION& screenInfo) auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); gci.SetActiveOutputBuffer(screenInfo); - // initialize cursor GH#4102 - Typically, the cursor is set to on by the - // cursor blinker. Unfortunately, in conpty mode, there is no cursor - // blinker. So, in conpty mode, we need to leave the cursor on always. The - // cursor can still be set to hidden, and whether the cursor should be - // blinking will still be passed through to the terminal, but internally, - // the cursor should always be on. - // - // In particular, some applications make use of a calling - // `SetConsoleScreenBuffer` and `SetCursorPosition` without printing any - // text in between these calls. If we initialize the cursor to Off in conpty - // mode, then the cursor will remain off until they print text. This can - // lead to alignment problems in the terminal, because we won't move the - // terminal's cursor in this _exact_ scenario. - screenInfo.GetTextBuffer().GetCursor().SetIsOn(gci.IsInVtIoMode()); - // set font screenInfo.RefreshFontWithRenderer(); diff --git a/src/host/renderData.cpp b/src/host/renderData.cpp index ffa7bfedeeb..fd6d5fc4dfc 100644 --- a/src/host/renderData.cpp +++ b/src/host/renderData.cpp @@ -6,15 +6,20 @@ #include "renderData.hpp" #include "dbcs.h" -#include "handle.h" #include "../interactivity/inc/ServiceLocator.hpp" #pragma hdrstop using namespace Microsoft::Console::Types; using namespace Microsoft::Console::Interactivity; +using namespace Microsoft::Console::Render; using Microsoft::Console::Interactivity::ServiceLocator; +void RenderData::UpdateSystemMetrics() +{ + _cursorBlinkInterval.reset(); +} + // Routine Description: // - Retrieves the viewport that applies over the data available in the GetTextBuffer() call // Return Value: @@ -96,93 +101,17 @@ void RenderData::UnlockConsole() noexcept gci.UnlockConsole(); } -// Method Description: -// - Gets the cursor's position in the buffer, relative to the buffer origin. -// Arguments: -// - -// Return Value: -// - the cursor's position in the buffer relative to the buffer origin. -til::point RenderData::GetCursorPosition() const noexcept -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); - return cursor.GetPosition(); -} - -// Method Description: -// - Returns whether the cursor is currently visible or not. If the cursor is -// visible and blinking, this is true, even if the cursor has currently -// blinked to the "off" state. -// Arguments: -// - -// Return Value: -// - true if the cursor is set to the visible state, regardless of blink state -bool RenderData::IsCursorVisible() const noexcept -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); - return cursor.IsVisible(); -} - -// Method Description: -// - Returns whether the cursor is currently visually visible or not. If the -// cursor is visible, and blinking, this will alternate between true and -// false as the cursor blinks. -// Arguments: -// - -// Return Value: -// - true if the cursor is currently visually visible, depending upon blink state -bool RenderData::IsCursorOn() const noexcept -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); - return cursor.IsVisible() && cursor.IsOn(); -} - -// Method Description: -// - The height of the cursor, out of 100, where 100 indicates the cursor should -// be the full height of the cell. -// Arguments: -// - -// Return Value: -// - height of the cursor, out of 100 -ULONG RenderData::GetCursorHeight() const noexcept +TimerDuration RenderData::GetBlinkInterval() noexcept { - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); - // Determine cursor height - auto ulHeight = cursor.GetSize(); - - // Now adjust the height for the overwrite/insert mode. If we're in overwrite mode, IsDouble will be set. - // When IsDouble is set, we either need to double the height of the cursor, or if it's already too big, - // then we need to shrink it by half. - if (cursor.IsDouble()) + if (!_cursorBlinkInterval) { - if (ulHeight > 50) // 50 because 50 percent is half of 100 percent which is the max size. - { - ulHeight >>= 1; - } - else - { - ulHeight <<= 1; - } + const auto enabled = ServiceLocator::LocateSystemConfigurationProvider()->IsCaretBlinkingEnabled(); + const auto interval = ServiceLocator::LocateSystemConfigurationProvider()->GetCaretBlinkTime(); + // >10s --> no blinking. The limit is arbitrary, because technically the valid range + // on Windows is 200-1200ms. GetCaretBlinkTime() returns INFINITE for no blinking, 0 for errors. + _cursorBlinkInterval = enabled && interval <= 10000 ? std ::chrono::milliseconds(interval) : TimerDuration::max(); } - - return ulHeight; -} - -// Method Description: -// - The CursorType of the cursor. The CursorType is used to determine what -// shape the cursor should be. -// Arguments: -// - -// Return Value: -// - the CursorType of the cursor. -CursorType RenderData::GetCursorStyle() const noexcept -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& cursor = gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor(); - return cursor.GetType(); + return *_cursorBlinkInterval; } // Method Description: @@ -197,26 +126,13 @@ ULONG RenderData::GetCursorPixelWidth() const noexcept return ServiceLocator::LocateGlobals().cursorPixelWidth; } -// Method Description: -// - Returns true if the cursor should be drawn twice as wide as usual because -// the cursor is currently over a cell with a double-wide character in it. -// Arguments: -// - -// Return Value: -// - true if the cursor should be drawn twice as wide as usual -bool RenderData::IsCursorDoubleWidth() const -{ - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - return gci.GetActiveOutputBuffer().CursorIsDoubleWidth(); -} - // Routine Description: // - Checks the user preference as to whether grid line drawing is allowed around the edges of each cell. // - This is for backwards compatibility with old behaviors in the legacy console. // Return Value: // - If true, line drawing information retrieved from the text buffer can/should be displayed. // - If false, it should be ignored and never drawn -const bool RenderData::IsGridLineDrawingAllowed() noexcept +bool RenderData::IsGridLineDrawingAllowed() noexcept { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); const auto outputMode = gci.GetActiveOutputBuffer().OutputMode; @@ -240,7 +156,7 @@ const bool RenderData::IsGridLineDrawingAllowed() noexcept // - Retrieves the title information to be displayed in the frame/edge of the window // Return Value: // - String with title information -const std::wstring_view RenderData::GetConsoleTitle() const noexcept +std::wstring_view RenderData::GetConsoleTitle() const noexcept { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); return gci.GetTitleAndPrefix(); @@ -252,7 +168,7 @@ const std::wstring_view RenderData::GetConsoleTitle() const noexcept // - The hyperlink ID // Return Value: // - The URI -const std::wstring RenderData::GetHyperlinkUri(uint16_t id) const +std::wstring RenderData::GetHyperlinkUri(uint16_t id) const { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); return gci.GetActiveOutputBuffer().GetTextBuffer().GetHyperlinkUriFromId(id); @@ -264,14 +180,14 @@ const std::wstring RenderData::GetHyperlinkUri(uint16_t id) const // - The hyperlink ID // Return Value: // - The custom ID if there was one, empty string otherwise -const std::wstring RenderData::GetHyperlinkCustomId(uint16_t id) const +std::wstring RenderData::GetHyperlinkCustomId(uint16_t id) const { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); return gci.GetActiveOutputBuffer().GetTextBuffer().GetCustomIdFromId(id); } // For now, we ignore regex patterns in conhost -const std::vector RenderData::GetPatternId(const til::point /*location*/) const +std::vector RenderData::GetPatternId(const til::point /*location*/) const { return {}; } @@ -293,12 +209,12 @@ std::pair RenderData::GetAttributeColors(const TextAttribute // - // Return Value: // - True if the selection variables contain valid selection data. False otherwise. -const bool RenderData::IsSelectionActive() const +bool RenderData::IsSelectionActive() const { return Selection::Instance().IsAreaSelected(); } -const bool RenderData::IsBlockSelection() const noexcept +bool RenderData::IsBlockSelection() const { return !Selection::Instance().IsLineSelection(); } @@ -338,7 +254,7 @@ const til::point_span* RenderData::GetSearchHighlightFocused() const noexcept // - none // Return Value: // - current selection anchor -const til::point RenderData::GetSelectionAnchor() const noexcept +til::point RenderData::GetSelectionAnchor() const noexcept { return Selection::Instance().GetSelectionAnchor(); } @@ -349,7 +265,7 @@ const til::point RenderData::GetSelectionAnchor() const noexcept // - none // Return Value: // - current selection anchor -const til::point RenderData::GetSelectionEnd() const noexcept +til::point RenderData::GetSelectionEnd() const noexcept { // The selection area in ConHost is encoded as two things... // - SelectionAnchor: the initial position where the selection was started @@ -381,3 +297,8 @@ const til::point RenderData::GetSelectionEnd() const noexcept return { x_pos, y_pos }; } + +bool RenderData::IsUiaDataInitialized() const noexcept +{ + return true; +} diff --git a/src/host/renderData.hpp b/src/host/renderData.hpp index 9d618cbf765..3567686d8fb 100644 --- a/src/host/renderData.hpp +++ b/src/host/renderData.hpp @@ -1,16 +1,5 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- renderData.hpp - -Abstract: -- This method provides an interface for rendering the final display based on the current console state - -Author(s): -- Michael Niksa (miniksa) Nov 2015 ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once @@ -20,41 +9,43 @@ class RenderData final : public Microsoft::Console::Render::IRenderData { public: + void UpdateSystemMetrics(); + + // + // BEGIN IRenderData + // + Microsoft::Console::Types::Viewport GetViewport() noexcept override; til::point GetTextBufferEndPosition() const noexcept override; TextBuffer& GetTextBuffer() const noexcept override; const FontInfo& GetFontInfo() const noexcept override; - + std::span GetSearchHighlights() const noexcept override; + const til::point_span* GetSearchHighlightFocused() const noexcept override; std::span GetSelectionSpans() const noexcept override; - void LockConsole() noexcept override; void UnlockConsole() noexcept override; - til::point GetCursorPosition() const noexcept override; - bool IsCursorVisible() const noexcept override; - bool IsCursorOn() const noexcept override; - ULONG GetCursorHeight() const noexcept override; - CursorType GetCursorStyle() const noexcept override; + Microsoft::Console::Render::TimerDuration GetBlinkInterval() noexcept override; ULONG GetCursorPixelWidth() const noexcept override; - bool IsCursorDoubleWidth() const override; - - const bool IsGridLineDrawingAllowed() noexcept override; - - const std::wstring_view GetConsoleTitle() const noexcept override; - - const std::wstring GetHyperlinkUri(uint16_t id) const override; - const std::wstring GetHyperlinkCustomId(uint16_t id) const override; - - const std::vector GetPatternId(const til::point location) const override; + bool IsGridLineDrawingAllowed() noexcept override; + std::wstring_view GetConsoleTitle() const noexcept override; + std::wstring GetHyperlinkUri(uint16_t id) const override; + std::wstring GetHyperlinkCustomId(uint16_t id) const override; + std::vector GetPatternId(const til::point location) const override; std::pair GetAttributeColors(const TextAttribute& attr) const noexcept override; - const bool IsSelectionActive() const override; - const bool IsBlockSelection() const noexcept override; + bool IsSelectionActive() const override; + bool IsBlockSelection() const override; void ClearSelection() override; void SelectNewRegion(const til::point coordStart, const til::point coordEnd) override; - std::span GetSearchHighlights() const noexcept override; - const til::point_span* GetSearchHighlightFocused() const noexcept override; - const til::point GetSelectionAnchor() const noexcept override; - const til::point GetSelectionEnd() const noexcept override; - const bool IsUiaDataInitialized() const noexcept override { return true; } + til::point GetSelectionAnchor() const noexcept override; + til::point GetSelectionEnd() const noexcept override; + bool IsUiaDataInitialized() const noexcept override; + + // + // END IRenderData + // + +private: + std::optional _cursorBlinkInterval; }; diff --git a/src/host/screenInfo.cpp b/src/host/screenInfo.cpp index 083aeb4ee4d..25fb0ed14a8 100644 --- a/src/host/screenInfo.cpp +++ b/src/host/screenInfo.cpp @@ -1311,12 +1311,6 @@ try // Also save the distance to the virtual bottom so it can be restored after the resize const auto cursorDistanceFromBottom = _virtualBottom - _textBuffer->GetCursor().GetPosition().y; - // skip any drawing updates that might occur until we swap _textBuffer with the new buffer or we exit early. - newTextBuffer->GetCursor().StartDeferDrawing(); - _textBuffer->GetCursor().StartDeferDrawing(); - // we're capturing _textBuffer by reference here because when we exit, we want to EndDefer on the current active buffer. - auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); - TextBuffer::Reflow(*_textBuffer.get(), *newTextBuffer.get()); // Since the reflow doesn't preserve the virtual bottom, we try and @@ -1357,8 +1351,6 @@ NT_CATCH_RETURN() [[nodiscard]] NTSTATUS SCREEN_INFORMATION::ResizeTraditional(const til::size coordNewScreenSize) try { - _textBuffer->GetCursor().StartDeferDrawing(); - auto endDefer = wil::scope_exit([&]() noexcept { _textBuffer->GetCursor().EndDeferDrawing(); }); _textBuffer->ResizeTraditional(coordNewScreenSize); return STATUS_SUCCESS; } @@ -1545,9 +1537,8 @@ void SCREEN_INFORMATION::SetCursorDBMode(const bool DoubleCursor) // - TurnOn - true if cursor should be left on, false if should be left off // Return Value: // - Status -[[nodiscard]] NTSTATUS SCREEN_INFORMATION::SetCursorPosition(const til::point Position, const bool TurnOn) +[[nodiscard]] NTSTATUS SCREEN_INFORMATION::SetCursorPosition(const til::point Position) { - const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); auto& cursor = _textBuffer->GetCursor(); // @@ -1581,20 +1572,6 @@ void SCREEN_INFORMATION::SetCursorDBMode(const bool DoubleCursor) _virtualBottom = Position.y; } - // if we have the focus, adjust the cursor state - if (gci.Flags & CONSOLE_HAS_FOCUS) - { - if (TurnOn) - { - cursor.SetDelay(false); - cursor.SetIsOn(true); - } - else - { - cursor.SetDelay(true); - } - } - return STATUS_SUCCESS; } @@ -1746,7 +1723,7 @@ const SCREEN_INFORMATION* SCREEN_INFORMATION::GetAltBuffer() const noexcept auto& altCursor = createdBuffer->GetTextBuffer().GetCursor(); altCursor.SetStyle(myCursor.GetSize(), myCursor.GetType()); altCursor.SetIsVisible(myCursor.IsVisible()); - altCursor.SetBlinkingAllowed(myCursor.IsBlinkingAllowed()); + altCursor.SetIsBlinking(myCursor.IsBlinking()); // The new position should match the viewport-relative position of the main buffer. auto altCursorPos = myCursor.GetPosition(); altCursorPos.y -= GetVirtualViewport().Top(); @@ -1895,7 +1872,7 @@ void SCREEN_INFORMATION::UseMainScreenBuffer() auto& mainCursor = psiMain->GetTextBuffer().GetCursor(); mainCursor.SetStyle(altCursor.GetSize(), altCursor.GetType()); mainCursor.SetIsVisible(altCursor.IsVisible()); - mainCursor.SetBlinkingAllowed(altCursor.IsBlinkingAllowed()); + mainCursor.SetIsBlinking(altCursor.IsBlinking()); // Copy the alt buffer's output mode back to the main buffer. psiMain->OutputMode = psiAlt->OutputMode; @@ -2336,19 +2313,6 @@ Viewport SCREEN_INFORMATION::GetVtPageArea() const noexcept return Viewport::FromExclusive({ 0, top, bufferWidth, top + viewportHeight }); } -// Method Description: -// - Returns true if the character at the cursor's current position is wide. -// Arguments: -// - -// Return Value: -// - true if the character at the cursor's current position is wide -bool SCREEN_INFORMATION::CursorIsDoubleWidth() const -{ - const auto& buffer = GetTextBuffer(); - const auto position = buffer.GetCursor().GetPosition(); - return buffer.GetRowByOffset(position.y).DbcsAttrAt(position.x) != DbcsAttribute::Single; -} - // Method Description: // - Gets the current font of the screen buffer. // Arguments: diff --git a/src/host/screenInfo.hpp b/src/host/screenInfo.hpp index 15f7ad65182..db3322d1a38 100644 --- a/src/host/screenInfo.hpp +++ b/src/host/screenInfo.hpp @@ -70,7 +70,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console const UINT uiCursorSize, _Outptr_ SCREEN_INFORMATION** const ppScreen); - ~SCREEN_INFORMATION(); + ~SCREEN_INFORMATION() override; void GetScreenBufferInformation(_Out_ til::size* pcoordSize, _Out_ til::point* pcoordCursorPosition, @@ -166,8 +166,6 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console InputBuffer* const GetActiveInputBuffer() const override; #pragma endregion - bool CursorIsDoubleWidth() const; - DWORD OutputMode; short WheelDelta; @@ -194,7 +192,7 @@ class SCREEN_INFORMATION : public ConsoleObjectHeader, public Microsoft::Console void SetCursorType(const CursorType Type, const bool setMain = false) noexcept; void SetCursorDBMode(const bool DoubleCursor); - [[nodiscard]] NTSTATUS SetCursorPosition(const til::point Position, const bool TurnOn); + [[nodiscard]] NTSTATUS SetCursorPosition(til::point Position); [[nodiscard]] NTSTATUS UseAlternateScreenBuffer(const TextAttribute& initAttributes); void UseMainScreenBuffer(); diff --git a/src/host/selection.cpp b/src/host/selection.cpp index c938a89bc5a..174782a056e 100644 --- a/src/host/selection.cpp +++ b/src/host/selection.cpp @@ -313,9 +313,6 @@ void Selection::_ExtendSelection(Selection::SelectionData* d, _In_ til::point co // - void Selection::_CancelMouseSelection() { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - auto& ScreenInfo = gci.GetActiveOutputBuffer(); - // invert old select rect. if we're selecting by mouse, we // always have a selection rect. HideSelection(); @@ -331,6 +328,8 @@ void Selection::_CancelMouseSelection() // Mark the cursor position as changed so we'll fire off a win event. // NOTE(lhecker): Why is this the only cancel function that would raise a WinEvent? Makes no sense to me. + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + auto& ScreenInfo = gci.GetActiveOutputBuffer(); auto& an = ServiceLocator::LocateGlobals().accessibilityNotifier; an.CursorChanged(ScreenInfo.GetTextBuffer().GetCursor().GetPosition(), false); } @@ -504,7 +503,7 @@ void Selection::InitializeMarkSelection() screenInfo.SetCursorInformation(100, TRUE); const auto coordPosition = cursor.GetPosition(); - LOG_IF_FAILED(screenInfo.SetCursorPosition(coordPosition, true)); + LOG_IF_FAILED(screenInfo.SetCursorPosition(coordPosition)); // set the cursor position as the anchor position // it will get updated as the cursor moves for mark mode, diff --git a/src/host/selectionState.cpp b/src/host/selectionState.cpp index 5ce107dea53..b769c3dc257 100644 --- a/src/host/selectionState.cpp +++ b/src/host/selectionState.cpp @@ -182,7 +182,6 @@ void Selection::_RestoreDataToCursor(Cursor& cursor) noexcept cursor.SetSize(_d->ulSavedCursorSize); cursor.SetIsVisible(_d->fSavedCursorVisible); cursor.SetType(_d->savedCursorType); - cursor.SetIsOn(true); cursor.SetPosition(_d->coordSavedCursorPosition); } diff --git a/src/host/server.h b/src/host/server.h index d318808a8c1..229ed5624a4 100644 --- a/src/host/server.h +++ b/src/host/server.h @@ -16,7 +16,6 @@ Revision History: #pragma once -#include "CursorBlinker.hpp" #include "IIoProvider.hpp" #include "readDataCooked.hpp" #include "settings.hpp" @@ -143,7 +142,6 @@ class CONSOLE_INFORMATION : friend void SetActiveScreenBuffer(_Inout_ SCREEN_INFORMATION& screenInfo); friend class SCREEN_INFORMATION; friend class CommonState; - Microsoft::Console::CursorBlinker& GetCursorBlinker() noexcept; MidiAudio& GetMidiAudio(); @@ -165,7 +163,6 @@ class CONSOLE_INFORMATION : std::optional _pendingClipboardText; Microsoft::Console::VirtualTerminal::VtIo _vtIo; - Microsoft::Console::CursorBlinker _blinker; MidiAudio _midiAudio; }; diff --git a/src/host/sources.inc b/src/host/sources.inc index 2d781343098..945ac81b10d 100644 --- a/src/host/sources.inc +++ b/src/host/sources.inc @@ -45,7 +45,6 @@ SOURCES = \ ..\selectionState.cpp \ ..\scrolling.cpp \ ..\cmdline.cpp \ - ..\CursorBlinker.cpp \ ..\alias.cpp \ ..\history.cpp \ ..\VtIo.cpp \ diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index 943d3199db6..a3c6a20ad11 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -3829,7 +3829,7 @@ void ScreenBufferTests::ScrollOperations() } Log::Comment(L"Set the cursor position and perform the operation."); - VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos)); stateMachine.ProcessString(escapeSequence.str()); // The cursor shouldn't move. @@ -3911,7 +3911,7 @@ void ScreenBufferTests::InsertReplaceMode() Log::Comment(L"Write additional content into a line of text with IRM mode enabled."); // Set the cursor position partway through the target row. - VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow })); // Enable Insert/Replace mode. stateMachine.ProcessString(L"\033[4h"); // Write out some new content. @@ -3936,7 +3936,7 @@ void ScreenBufferTests::InsertReplaceMode() Log::Comment(L"Write additional content into a line of text with IRM mode disabled."); // Set the cursor position partway through the target row. - VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ targetCol, targetRow })); // Disable Insert/Replace mode. stateMachine.ProcessString(L"\033[4l"); // Write out some new content. @@ -3999,7 +3999,7 @@ void ScreenBufferTests::InsertChars() auto insertPos = til::CoordType{ 20 }; // Place the cursor in the center of the line. - VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine })); // Save the cursor position. It shouldn't move for the rest of the test. const auto& cursor = si.GetTextBuffer().GetCursor(); @@ -4067,7 +4067,7 @@ void ScreenBufferTests::InsertChars() // Move cursor to right edge. insertPos = horizontalMarginsActive ? viewportEnd - 1 : bufferWidth - 1; - VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine })); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. @@ -4116,7 +4116,7 @@ void ScreenBufferTests::InsertChars() // Move cursor to left edge. insertPos = horizontalMarginsActive ? viewportStart : 0; - VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ insertPos, insertLine })); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. @@ -4199,7 +4199,7 @@ void ScreenBufferTests::DeleteChars() auto deletePos = til::CoordType{ 20 }; // Place the cursor in the center of the line. - VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine })); // Save the cursor position. It shouldn't move for the rest of the test. const auto& cursor = si.GetTextBuffer().GetCursor(); @@ -4267,7 +4267,7 @@ void ScreenBufferTests::DeleteChars() // Move cursor to right edge. deletePos = horizontalMarginsActive ? viewportEnd - 1 : bufferWidth - 1; - VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine })); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. @@ -4316,7 +4316,7 @@ void ScreenBufferTests::DeleteChars() // Move cursor to left edge. deletePos = horizontalMarginsActive ? viewportStart : 0; - VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ deletePos, deleteLine })); expectedCursor = cursor.GetPosition(); // Fill the entire line with Qs. Blue on Green. @@ -4484,7 +4484,7 @@ void ScreenBufferTests::ScrollingWideCharsHorizontally() _FillLine(testRow, testChars, testAttr); Log::Comment(L"Position the cursor at the start of the test row"); - VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, testRow }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, testRow })); Log::Comment(L"Insert 1 cell at the start of the test row"); stateMachine.ProcessString(L"\033[@"); @@ -4552,7 +4552,7 @@ void ScreenBufferTests::EraseScrollbackTests() const auto cursorPos = til::point{ centerX, centerY }; Log::Comment(L"Set the cursor position and erase the scrollback."); - VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos)); stateMachine.ProcessString(L"\x1b[3J"); // The viewport should move to the top of the buffer, while the cursor @@ -4682,7 +4682,7 @@ void ScreenBufferTests::EraseTests() const auto centerY = (viewport.Top() + viewport.BottomExclusive()) / 2; Log::Comment(L"Set the cursor position and perform the operation."); - VERIFY_SUCCEEDED(si.SetCursorPosition({ centerX, centerY }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ centerX, centerY })); stateMachine.ProcessString(escapeSequence.str()); // Get cursor position and viewport range. @@ -5756,7 +5756,7 @@ void ScreenBufferTests::HardResetBuffer() si.SetAttributes(TextAttribute()); si.ClearTextData(); VERIFY_SUCCEEDED(si.SetViewportOrigin(true, { 0, 0 }, true)); - VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, 0 }, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition({ 0, 0 })); VERIFY_IS_TRUE(isBufferClear()); Log::Comment(L"Write a single line of text to the buffer"); @@ -5983,7 +5983,7 @@ void ScreenBufferTests::ClearAlternateBuffer() auto useMain = wil::scope_exit([&] { altBuffer.UseMainScreenBuffer(); }); // Set the position to home; otherwise, it's inherited from the main buffer. - VERIFY_SUCCEEDED(altBuffer.SetCursorPosition({ 0, 0 }, true)); + VERIFY_SUCCEEDED(altBuffer.SetCursorPosition({ 0, 0 })); WriteText(altBuffer.GetTextBuffer()); VerifyText(altBuffer.GetTextBuffer()); @@ -7023,7 +7023,7 @@ void ScreenBufferTests::ScreenAlignmentPattern() // Place the cursor in the center. auto cursorPos = til::point{ bufferWidth / 2, (viewportStart + viewportEnd) / 2 }; - VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos, true)); + VERIFY_SUCCEEDED(si.SetCursorPosition(cursorPos)); Log::Comment(L"Execute the DECALN escape sequence."); stateMachine.ProcessString(L"\x1b#8"); diff --git a/src/inc/til/small_vector.h b/src/inc/til/small_vector.h index 027f0484b5a..ab13ea65684 100644 --- a/src/inc/til/small_vector.h +++ b/src/inc/til/small_vector.h @@ -938,4 +938,15 @@ namespace til }; } +namespace std +{ + template + void erase_if(til::small_vector& vec, F&& pred) + { + const auto beg = vec.begin(); + const auto end = vec.end(); + vec.erase(std::remove_if(beg, end, std::forward(pred)), end); + } +} + #pragma warning(pop) diff --git a/src/interactivity/onecore/SystemConfigurationProvider.cpp b/src/interactivity/onecore/SystemConfigurationProvider.cpp index 7e5b7c59abf..ea18da7b328 100644 --- a/src/interactivity/onecore/SystemConfigurationProvider.cpp +++ b/src/interactivity/onecore/SystemConfigurationProvider.cpp @@ -9,12 +9,12 @@ using namespace Microsoft::Console::Interactivity::OneCore; UINT SystemConfigurationProvider::GetCaretBlinkTime() noexcept { - return s_DefaultCaretBlinkTime; + return 530; // milliseconds } bool SystemConfigurationProvider::IsCaretBlinkingEnabled() noexcept { - return s_DefaultIsCaretBlinkingEnabled; + return true; } int SystemConfigurationProvider::GetNumberOfMouseButtons() noexcept @@ -25,23 +25,23 @@ int SystemConfigurationProvider::GetNumberOfMouseButtons() noexcept } else { - return s_DefaultNumberOfMouseButtons; + return 3; } } ULONG SystemConfigurationProvider::GetCursorWidth() noexcept { - return s_DefaultCursorWidth; + return 1; } ULONG SystemConfigurationProvider::GetNumberOfWheelScrollLines() noexcept { - return s_DefaultNumberOfWheelScrollLines; + return 3; } ULONG SystemConfigurationProvider::GetNumberOfWheelScrollCharacters() noexcept { - return s_DefaultNumberOfWheelScrollCharacters; + return 3; } void SystemConfigurationProvider::GetSettingsFromLink( diff --git a/src/interactivity/onecore/SystemConfigurationProvider.hpp b/src/interactivity/onecore/SystemConfigurationProvider.hpp index 054e8053af6..e780dfb78ee 100644 --- a/src/interactivity/onecore/SystemConfigurationProvider.hpp +++ b/src/interactivity/onecore/SystemConfigurationProvider.hpp @@ -41,13 +41,6 @@ namespace Microsoft::Console::Interactivity::OneCore _Inout_opt_ IconInfo* iconInfo) override; private: - static constexpr UINT s_DefaultCaretBlinkTime = 530; // milliseconds - static constexpr bool s_DefaultIsCaretBlinkingEnabled = true; - static constexpr int s_DefaultNumberOfMouseButtons = 3; - static constexpr ULONG s_DefaultCursorWidth = 1; - static constexpr ULONG s_DefaultNumberOfWheelScrollLines = 3; - static constexpr ULONG s_DefaultNumberOfWheelScrollCharacters = 3; - friend class ::InputTests; }; } diff --git a/src/interactivity/win32/SystemConfigurationProvider.cpp b/src/interactivity/win32/SystemConfigurationProvider.cpp index 58472898d1c..f18370e61d4 100644 --- a/src/interactivity/win32/SystemConfigurationProvider.cpp +++ b/src/interactivity/win32/SystemConfigurationProvider.cpp @@ -35,7 +35,7 @@ ULONG SystemConfigurationProvider::GetCursorWidth() else { LOG_LAST_ERROR(); - return s_DefaultCursorWidth; + return 1; } } diff --git a/src/interactivity/win32/SystemConfigurationProvider.hpp b/src/interactivity/win32/SystemConfigurationProvider.hpp index 9f743cb00a2..4fd90932f37 100644 --- a/src/interactivity/win32/SystemConfigurationProvider.hpp +++ b/src/interactivity/win32/SystemConfigurationProvider.hpp @@ -39,8 +39,5 @@ namespace Microsoft::Console::Interactivity::Win32 _In_ PCWSTR pwszCurrDir, _In_ PCWSTR pwszAppName, _Inout_opt_ IconInfo* iconInfo); - - private: - static const ULONG s_DefaultCursorWidth = 1; }; } diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp index e96848fe3bf..5ef3c1c514e 100644 --- a/src/interactivity/win32/window.cpp +++ b/src/interactivity/win32/window.cpp @@ -167,15 +167,12 @@ void Window::_UpdateSystemMetrics() const { const auto dpiApi = ServiceLocator::LocateHighDpiApi(); auto& g = ServiceLocator::LocateGlobals(); - auto& gci = g.getConsoleInformation(); Scrolling::s_UpdateSystemMetrics(); g.sVerticalScrollSize = dpiApi->GetSystemMetricsForDpi(SM_CXVSCROLL, g.dpi); g.sHorizontalScrollSize = dpiApi->GetSystemMetricsForDpi(SM_CYHSCROLL, g.dpi); - gci.GetCursorBlinker().UpdateSystemMetrics(); - const auto sysConfig = ServiceLocator::LocateSystemConfigurationProvider(); g.cursorPixelWidth = sysConfig->GetCursorWidth(); diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp index 9bf6f1df88a..35ef5fef4e6 100644 --- a/src/interactivity/win32/windowproc.cpp +++ b/src/interactivity/win32/windowproc.cpp @@ -27,17 +27,17 @@ struct TsfDataProvider : Microsoft::Console::TSF::IDataProvider { virtual ~TsfDataProvider() = default; - STDMETHODIMP TsfDataProvider::QueryInterface(REFIID, void**) noexcept override + STDMETHODIMP QueryInterface(REFIID, void**) noexcept override { return E_NOTIMPL; } - ULONG STDMETHODCALLTYPE TsfDataProvider::AddRef() noexcept override + ULONG STDMETHODCALLTYPE AddRef() noexcept override { return 1; } - ULONG STDMETHODCALLTYPE TsfDataProvider::Release() noexcept override + ULONG STDMETHODCALLTYPE Release() noexcept override { return 1; } @@ -287,12 +287,12 @@ static constexpr TsfDataProvider s_tsfDataProvider; case WM_SETFOCUS: { gci.ProcessHandleList.ModifyConsoleProcessFocus(TRUE); - gci.Flags |= CONSOLE_HAS_FOCUS; - gci.GetCursorBlinker().FocusStart(); - - HandleFocusEvent(TRUE); + if (const auto renderer = ServiceLocator::LocateGlobals().pRender) + { + renderer->AllowCursorVisibility(Render::InhibitionSource::Host, true); + } if (!g.tsf) { @@ -306,21 +306,21 @@ static constexpr TsfDataProvider s_tsfDataProvider; LOG_IF_FAILED(_pUiaProvider->SetTextAreaFocus()); } + HandleFocusEvent(TRUE); break; } case WM_KILLFOCUS: { gci.ProcessHandleList.ModifyConsoleProcessFocus(FALSE); - gci.Flags &= ~CONSOLE_HAS_FOCUS; - // turn it off when we lose focus. - gci.GetActiveOutputBuffer().GetTextBuffer().GetCursor().SetIsOn(false); - gci.GetCursorBlinker().FocusEnd(); + if (const auto renderer = ServiceLocator::LocateGlobals().pRender) + { + renderer->AllowCursorVisibility(Render::InhibitionSource::Host, false); + } HandleFocusEvent(FALSE); - break; } @@ -364,9 +364,9 @@ static constexpr TsfDataProvider s_tsfDataProvider; case WM_SETTINGCHANGE: { LOG_IF_FAILED(Microsoft::Console::Internal::Theming::TrySetDarkMode(hWnd)); - gci.GetCursorBlinker().SettingsChanged(); - } + gci.renderData.UpdateSystemMetrics(); __fallthrough; + } case WM_DISPLAYCHANGE: { diff --git a/src/renderer/base/RenderSettings.cpp b/src/renderer/base/RenderSettings.cpp index c587c60128d..292f86faf6a 100644 --- a/src/renderer/base/RenderSettings.cpp +++ b/src/renderer/base/RenderSettings.cpp @@ -59,11 +59,6 @@ void RenderSettings::RestoreDefaultSettings() noexcept void RenderSettings::SetRenderMode(const Mode mode, const bool enabled) noexcept { _renderMode.set(mode, enabled); - // If blinking is disabled, make sure blinking content is not faint. - if (mode == Mode::BlinkAllowed && !enabled) - { - _blinkShouldBeFaint = false; - } } // Routine Description: @@ -187,8 +182,6 @@ void RenderSettings::RestoreDefaultColorAliasIndex(const ColorAlias alias) noexc // - The color values of the attribute's foreground and background. std::pair RenderSettings::GetAttributeColors(const TextAttribute& attr) const noexcept { - _blinkIsInUse = _blinkIsInUse || attr.IsBlinking(); - const auto fgTextColor = attr.GetForeground(); const auto bgTextColor = attr.GetBackground(); @@ -296,35 +289,7 @@ COLORREF RenderSettings::GetAttributeUnderlineColor(const TextAttribute& attr) c return ul; } -// Routine Description: -// - Increments the position in the blink cycle, toggling the blink rendition -// state on every second call, potentially triggering a redraw of the given -// renderer if there are blinking cells currently in view. -// Arguments: -// - renderer: the renderer that will be redrawn. -void RenderSettings::ToggleBlinkRendition(Renderer* renderer) noexcept -try +void RenderSettings::ToggleBlinkRendition() noexcept { - if (GetRenderMode(Mode::BlinkAllowed)) - { - // This method is called with the frequency of the cursor blink rate, - // but we only want our cells to blink at half that frequency. We thus - // have a blink cycle that loops through four phases... - _blinkCycle = (_blinkCycle + 1) % 4; - // ... and two of those four render the blink attributes as faint. - _blinkShouldBeFaint = _blinkCycle >= 2; - // Every two cycles (when the state changes), we need to trigger a - // redraw, but only if there are actually blink attributes in use. - if (_blinkIsInUse && _blinkCycle % 2 == 0) - { - // We reset the _blinkIsInUse flag before redrawing, so we can - // get a fresh assessment of the current blink attribute usage. - _blinkIsInUse = false; - if (renderer) - { - renderer->TriggerRedrawAll(); - } - } - } + _blinkShouldBeFaint = !_blinkShouldBeFaint; } -CATCH_LOG() diff --git a/src/renderer/base/lib/base.vcxproj b/src/renderer/base/lib/base.vcxproj index 3c177d955e2..7520ca9c6ef 100644 --- a/src/renderer/base/lib/base.vcxproj +++ b/src/renderer/base/lib/base.vcxproj @@ -19,7 +19,6 @@ - Create @@ -39,9 +38,8 @@ - - + \ No newline at end of file diff --git a/src/renderer/base/lib/base.vcxproj.filters b/src/renderer/base/lib/base.vcxproj.filters index 8887f1c610f..5bee2b8f75f 100644 --- a/src/renderer/base/lib/base.vcxproj.filters +++ b/src/renderer/base/lib/base.vcxproj.filters @@ -33,9 +33,6 @@ Source Files - - Source Files - Source Files @@ -56,9 +53,6 @@ Header Files - - Header Files - Header Files\inc @@ -98,5 +92,6 @@ + - + \ No newline at end of file diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 9567b8cd49b..15bf1b3b303 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -4,20 +4,17 @@ #include "precomp.h" #include "renderer.hpp" +#include + using namespace Microsoft::Console::Render; using namespace Microsoft::Console::Types; using PointTree = interval_tree::IntervalTree; -static constexpr auto maxRetriesForRenderEngine = 3; +static constexpr TimerRepr TimerReprMax = std::numeric_limits::max(); +static constexpr DWORD maxRetriesForRenderEngine = 3; // The renderer will wait this number of milliseconds * how many tries have elapsed before trying again. -static constexpr auto renderBackoffBaseTimeMilliseconds{ 150 }; - -#define FOREACH_ENGINE(var) \ - for (auto var : _engines) \ - if (!var) \ - break; \ - else +static constexpr DWORD renderBackoffBaseTimeMilliseconds = 150; // Routine Description: // - Creates a new renderer controller for a console. @@ -29,6 +26,18 @@ Renderer::Renderer(RenderSettings& renderSettings, IRenderData* pData) : _renderSettings(renderSettings), _pData(pData) { + _cursorBlinker = RegisterTimer("cursor blink", [](Renderer& renderer, TimerHandle) { + renderer._cursorBlinkerOn = !renderer._cursorBlinkerOn; + }); + _renditionBlinker = RegisterTimer("blink rendition", [](Renderer& renderer, TimerHandle) { + renderer._renderSettings.ToggleBlinkRendition(); + renderer.TriggerRedrawAll(); + }); +} + +Renderer::~Renderer() +{ + TriggerTeardown(); } IRenderData* Renderer::GetRenderData() const noexcept @@ -36,6 +45,279 @@ IRenderData* Renderer::GetRenderData() const noexcept return _pData; } +// Routine Description: +// - Sets an event in the render thread that allows it to proceed, thus enabling painting. +// Arguments: +// - +// Return Value: +// - +void Renderer::EnablePainting() +{ + // When the renderer is constructed, the initial viewport won't be available yet, + // but once EnablePainting is called it should be safe to retrieve. + _viewport = _pData->GetViewport(); + + _enable.SetEvent(); + + if (const auto guard = _threadMutex.lock_exclusive(); !_thread) + { + _threadKeepRunning.store(true, std::memory_order_relaxed); + + _thread.reset(CreateThread(nullptr, 0, s_renderThread, this, 0, nullptr)); + THROW_LAST_ERROR_IF(!_thread); + + // SetThreadDescription only works on 1607 and higher. If we cannot find it, + // then it's no big deal. Just skip setting the description. + const auto func = GetProcAddressByFunctionDeclaration(GetModuleHandleW(L"kernel32.dll"), SetThreadDescription); + if (func) + { + LOG_IF_FAILED(func(_thread.get(), L"Rendering Output Thread")); + } + } +} + +void Renderer::_disablePainting() noexcept +{ + _enable.ResetEvent(); +} + +// Method Description: +// - Called when the host is about to die, to give the renderer one last chance +// to paint before the host exits. +// Arguments: +// - +// Return Value: +// - +void Renderer::TriggerTeardown() noexcept +{ + if (const auto guard = _threadMutex.lock_exclusive(); _thread) + { + // The render thread first waits for the event and then checks _threadKeepRunning. By doing it + // in reverse order here, we ensure that it's impossible for the render thread to miss this. + _threadKeepRunning.store(false, std::memory_order_relaxed); + NotifyPaintFrame(); + _enable.SetEvent(); + + WaitForSingleObject(_thread.get(), INFINITE); + _thread.reset(); + } + + _disablePainting(); +} + +void Renderer::NotifyPaintFrame() noexcept +{ + _redraw.store(true, std::memory_order_relaxed); + til::atomic_notify_one(_redraw); +} + +DWORD WINAPI Renderer::s_renderThread(void* param) noexcept +{ + return static_cast(param)->_renderThread(); +} + +DWORD Renderer::_renderThread() noexcept +{ + while (true) + { + _enable.wait(); + _waitUntilCanRender(); + _waitUntilTimerOrRedraw(); + + if (!_threadKeepRunning.load(std::memory_order_relaxed)) + { + break; + } + + LOG_IF_FAILED(PaintFrame()); + } + + return S_OK; +} + +void Renderer::_waitUntilCanRender() noexcept +{ + for (const auto pEngine : _engines) + { + pEngine->WaitUntilCanRender(); + } +} + +TimerHandle Renderer::RegisterTimer(const char* description, TimerCallback routine) +{ + // If it doesn't crash now, it would crash later. + WI_ASSERT(routine != nullptr); + + const auto id = _nextTimerId++; + + _timers.push_back(TimerRoutine{ + .description = description, + .interval = TimerReprMax, + .next = TimerReprMax, + .routine = std::move(routine), + }); + + return TimerHandle{ id }; +} + +bool Renderer::IsTimerRunning(TimerHandle handle) const +{ + const auto& timer = _timers.at(handle.id); + return timer.next != TimerReprMax; +} + +TimerDuration Renderer::GetTimerInterval(TimerHandle handle) const +{ + const auto& timer = _timers.at(handle.id); + return TimerDuration{ timer.interval }; +} + +void Renderer::StarTimer(TimerHandle handle, TimerDuration delay) +{ + _starTimer(handle, delay.count(), TimerReprMax); +} + +void Renderer::StartRepeatingTimer(TimerHandle handle, TimerDuration interval) +{ + _starTimer(handle, interval.count(), interval.count()); +} + +void Renderer::_starTimer(TimerHandle handle, TimerRepr delay, TimerRepr interval) +{ + // Nothing breaks if these assertions are violated, but you should still violate them. + // A timer with a 1-hour delay is weird and indicative of a bug. It should have been + // a max-wait (TimerReprMax) instead, which turns into an INFINITE timeout + // for WaitOnAddress(), which in turn is less costly than one with timeout. +#ifndef NDEBUG + constexpr TimerRepr one_min_in_100ns = 60 * 1000 * 10000; + assert(delay > 0 && (delay < one_min_in_100ns || delay == TimerReprMax)); + assert(interval > 0 && (interval < one_min_in_100ns || interval == TimerReprMax)); +#endif + + auto& timer = _timers.at(handle.id); + timer.interval = interval; + timer.next = _timerSaturatingAdd(_timerInstant(), delay); + + // Tickle _waitUntilCanRender() into calling _calculateTimerMaxWait() again. + // WaitOnAddress() will return with TRUE, even if the atomic didn't change. + til::atomic_notify_one(_redraw); +} + +void Renderer::StopTimer(TimerHandle handle) +{ + auto& timer = _timers.at(handle.id); + timer.interval = TimerReprMax; + timer.next = TimerReprMax; +} + +DWORD Renderer::_calculateTimerMaxWait() noexcept +{ + if (_timers.empty()) + { + return INFINITE; + } + + const auto now = _timerInstant(); + auto wait = TimerReprMax; + + for (const auto& timer : _timers) + { + wait = std::min(wait, _timerSaturatingSub(timer.next, now)); + } + + return _timerToMillis(wait); +} + +void Renderer::_waitUntilTimerOrRedraw() noexcept +{ + // Did we get an explicit rendering request? Yes? Exit. + // + // We don't reset _redraw just yet because we can delay that until we + // actually acquired the console lock. That's the main synchronization + // point and the instant we know everyone else is blocked. See PaintFrame(). + while (!_redraw.load(std::memory_order_relaxed)) + { + // Otherwise calculate when the next timer expires. + const auto wait = _calculateTimerMaxWait(); + if (wait == 0) + { + break; + } + + // and wait until the timer expires, or we potentially got a rendering request. + constexpr auto bad = false; + if (!til::atomic_wait(_redraw, bad, wait)) + { + // The timer expired. + assert(GetLastError() == ERROR_TIMEOUT); // What else could it be? + break; + } + + // If WaitOnAddress returned TRUE, we got signaled and retry. + } +} + +void Renderer::_tickTimers() noexcept +{ + const auto now = _timerInstant(); + size_t id = 0; + + for (auto& timer : _timers) + { + if (now >= timer.next) + { + // Prevent clock drift by incrementing the originally scheduled time. + timer.next = _timerSaturatingAdd(timer.next, timer.interval); + // ...but still take care to not schedule in the past. + if (timer.next <= now) + { + timer.next = now + timer.interval; + } + + try + { + timer.routine(*this, TimerHandle{ id }); + } + CATCH_LOG(); + } + + id++; + } +} + +ULONGLONG Renderer::_timerInstant() noexcept +{ + // QueryUnbiasedInterruptTime is what WaitOnAddress uses internally. + ULONGLONG now; + QueryUnbiasedInterruptTime(&now); + return now; +} + +TimerRepr Renderer::_timerSaturatingAdd(TimerRepr a, TimerRepr b) noexcept +{ + auto c = a + b; + if (c < a) + { + c = TimerReprMax; + } + return c; +} + +TimerRepr Renderer::_timerSaturatingSub(TimerRepr a, TimerRepr b) noexcept +{ + auto c = a - b; + if (c > a) + { + c = 0; + } + return c; +} + +DWORD Renderer::_timerToMillis(TimerRepr t) noexcept +{ + return gsl::narrow_cast(std::min(t / 10000, DWORD_MAX)); +} + // Routine Description: // - Walks through the console data structures to compose a new frame based on the data that has changed since last call and outputs it to the connected rendering engine. // Arguments: @@ -61,7 +343,7 @@ IRenderData* Renderer::GetRenderData() const noexcept if (--tries == 0) { // Stop trying. - _thread.DisablePainting(); + _disablePainting(); if (_pfnRendererEnteredErrorState) { _pfnRendererEnteredErrorState(); @@ -93,25 +375,36 @@ IRenderData* Renderer::GetRenderData() const noexcept _synchronizeWithOutput(); } - // Last chance check if anything scrolled without an explicit invalidate notification since the last frame. + _tickTimers(); + + // We reset _redraw after _tickTimers() so that NotifyPaintFrame() calls + // are picked up and ignored. We're about to render a frame after all. + // We do it before the remaining code below so that if we do have an + // intentional call to NotifyPaintFrame(), it triggers a redraw. + _redraw.store(false, std::memory_order_relaxed); + + // NOTE: _CheckViewportAndScroll() updates _viewport which is used by all other functions. _CheckViewportAndScroll(); - _invalidateCurrentCursor(); // Invalidate the previous cursor position. + _scheduleRenditionBlink(); + + // Add the previous cursor / composition to the dirty rect. + _invalidateCurrentCursor(); _invalidateOldComposition(); + // Add the new cursor position to the dirt rect. + // Prepare the composition for insertion into the output screen. _updateCursorInfo(); - _compositionCache.reset(); - - _invalidateCurrentCursor(); // Invalidate the new cursor position. + _invalidateCurrentCursor(); // NOTE: This now refers to the updated cursor position. _prepareNewComposition(); - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { RETURN_IF_FAILED(_PaintFrameForEngine(pEngine)); } } - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { RETURN_IF_FAILED(pEngine->Present()); } @@ -180,12 +473,6 @@ try } CATCH_RETURN() -void Renderer::NotifyPaintFrame() noexcept -{ - // The thread will provide throttling for us. - _thread.NotifyPaint(); -} - // NOTE: You must be holding the console lock when calling this function. void Renderer::SynchronizedOutputChanged() noexcept { @@ -257,6 +544,28 @@ void Renderer::_synchronizeWithOutput() noexcept _renderSettings.SetRenderMode(RenderSettings::Mode::SynchronizedOutput, false); } +void Renderer::AllowCursorVisibility(InhibitionSource source, bool enable) noexcept +{ + const auto before = _cursorVisibilityInhibitors.any(); + _cursorVisibilityInhibitors.set(source, !enable); + const auto after = _cursorVisibilityInhibitors.any(); + if (before != after) + { + NotifyPaintFrame(); + } +} + +void Renderer::AllowCursorBlinking(InhibitionSource source, bool enable) noexcept +{ + const auto before = _cursorBlinkingInhibitors.any(); + _cursorBlinkingInhibitors.set(source, !enable); + const auto after = _cursorBlinkingInhibitors.any(); + if (before != after) + { + NotifyPaintFrame(); + } +} + // Routine Description: // - Called when the system has requested we redraw a portion of the console. // Arguments: @@ -265,7 +574,7 @@ void Renderer::_synchronizeWithOutput() noexcept // - void Renderer::TriggerSystemRedraw(const til::rect* const prcDirtyClient) { - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->InvalidateSystem(prcDirtyClient)); } @@ -299,7 +608,7 @@ void Renderer::TriggerRedraw(const Viewport& region) if (view.TrimToViewport(&srUpdateRegion)) { view.ConvertToOrigin(&srUpdateRegion); - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->Invalidate(&srUpdateRegion)); } @@ -329,7 +638,7 @@ void Renderer::TriggerRedraw(const til::point* const pcoord) // - void Renderer::TriggerRedrawAll(const bool backgroundChanged, const bool frameChanged) { - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->InvalidateAll()); } @@ -347,19 +656,6 @@ void Renderer::TriggerRedrawAll(const bool backgroundChanged, const bool frameCh } } -// Method Description: -// - Called when the host is about to die, to give the renderer one last chance -// to paint before the host exits. -// Arguments: -// - -// Return Value: -// - -void Renderer::TriggerTeardown() noexcept -{ - // We need to shut down the paint thread on teardown. - _thread.TriggerTeardown(); -} - // Routine Description: // - Called when the selected area in the console has changed. // Arguments: @@ -394,7 +690,7 @@ try } } - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->InvalidateSelection(_lastSelectionRectsByViewport)); LOG_IF_FAILED(pEngine->InvalidateSelection(newSelectionViewportRects)); @@ -423,7 +719,7 @@ try const auto& buffer = _pData->GetTextBuffer(); - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->InvalidateHighlight(oldHighlights, buffer)); LOG_IF_FAILED(pEngine->InvalidateHighlight(newHighlights, buffer)); @@ -456,7 +752,7 @@ bool Renderer::_CheckViewportAndScroll() coordDelta.x = srOldViewport.left - srNewViewport.left; coordDelta.y = srOldViewport.top - srNewViewport.top; - FOREACH_ENGINE(engine) + for (const auto engine : _engines) { LOG_IF_FAILED(engine->UpdateViewport(srNewViewport)); LOG_IF_FAILED(engine->InvalidateScroll(&coordDelta)); @@ -485,6 +781,38 @@ bool Renderer::_CheckViewportAndScroll() return true; } +void Renderer::_scheduleRenditionBlink() +{ + const auto& buffer = _pData->GetTextBuffer(); + bool blinkUsed = false; + + for (auto row = _viewport.Top(); row < _viewport.BottomExclusive(); ++row) + { + const auto& r = buffer.GetRowByOffset(row); + for (const auto& attr : r.Attributes()) + { + if (attr.IsBlinking()) + { + blinkUsed = true; + goto why_does_cpp_not_have_labeled_loops; + } + } + } + +why_does_cpp_not_have_labeled_loops: + if (blinkUsed != IsTimerRunning(_renditionBlinker)) + { + if (blinkUsed) + { + StartRepeatingTimer(_renditionBlinker, std::chrono::seconds(1)); + } + else + { + StopTimer(_renditionBlinker); + } + } +} + // Routine Description: // - Called when a scroll operation has occurred by manipulating the viewport. // - This is a special case as calling out scrolls explicitly drastically improves performance. @@ -511,7 +839,7 @@ void Renderer::TriggerScroll() // - void Renderer::TriggerScroll(const til::point* const pcoordDelta) { - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->InvalidateScroll(pcoordDelta)); } @@ -531,7 +859,7 @@ void Renderer::TriggerScroll(const til::point* const pcoordDelta) void Renderer::TriggerTitleChange() { const auto newTitle = _pData->GetConsoleTitle(); - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->InvalidateTitle(newTitle)); } @@ -540,7 +868,7 @@ void Renderer::TriggerTitleChange() void Renderer::TriggerNewTextNotification(const std::wstring_view newText) { - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->NotifyNewText(newText)); } @@ -568,7 +896,7 @@ HRESULT Renderer::_PaintTitle(IRenderEngine* const pEngine) // - void Renderer::TriggerFontChange(const int iDpi, const FontInfoDesired& FontInfoDesired, _Out_ FontInfo& FontInfo) { - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->UpdateDpi(iDpi)); LOG_IF_FAILED(pEngine->UpdateFont(FontInfoDesired, FontInfo)); @@ -594,7 +922,7 @@ void Renderer::UpdateSoftFont(const std::span bitPattern, const const auto softFontCharCount = cellSize.height ? bitPattern.size() / cellSize.height : 0; _lastSoftFontChar = _firstSoftFontChar + softFontCharCount - 1; - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->UpdateSoftFont(bitPattern, cellSize, centeringHint)); } @@ -624,7 +952,7 @@ bool Renderer::s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSo // renderer. We won't know which is which, so iterate over them. // Only return the result of the successful one if it's not S_FALSE (which is the VT renderer) // TODO: 14560740 - The Window might be able to get at this info in a more sane manner - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { const auto hr = LOG_IF_FAILED(pEngine->GetProposedFont(FontInfoDesired, FontInfo, iDpi)); // We're looking for specifically S_OK, S_FALSE is not good enough. @@ -655,7 +983,7 @@ bool Renderer::IsGlyphWideByFont(const std::wstring_view glyph) // renderer. We won't know which is which, so iterate over them. // Only return the result of the successful one if it's not S_FALSE (which is the VT renderer) // TODO: 14560740 - The Window might be able to get at this info in a more sane manner - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { const auto hr = LOG_IF_FAILED(pEngine->IsGlyphWideByFont(glyph, &fIsFullWidth)); // We're looking for specifically S_OK, S_FALSE is not good enough. @@ -668,20 +996,6 @@ bool Renderer::IsGlyphWideByFont(const std::wstring_view glyph) return fIsFullWidth; } -// Routine Description: -// - Sets an event in the render thread that allows it to proceed, thus enabling painting. -// Arguments: -// - -// Return Value: -// - -void Renderer::EnablePainting() -{ - // When the renderer is constructed, the initial viewport won't be available yet, - // but once EnablePainting is called it should be safe to retrieve. - _viewport = _pData->GetViewport(); - _thread.EnablePainting(); -} - // Routine Description: // - Paint helper to fill in the background color of the invalid area within the frame. // Arguments: @@ -706,7 +1020,6 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // This is the subsection of the entire screen buffer that is currently being presented. // It can move left/right or top/bottom depending on how the viewport is scrolled // relative to the entire buffer. - const auto view = _pData->GetViewport(); const auto compositionRow = _compositionCache ? _compositionCache->absoluteOrigin.y : -1; const auto& activeComposition = _pData->GetActiveComposition(); @@ -731,12 +1044,12 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // Shift the origin of the dirty region to match the underlying buffer so we can // compare the two regions directly for intersection. - dirty = Viewport::Offset(dirty, view.Origin()); + dirty = Viewport::Offset(dirty, _viewport.Origin()); // The intersection between what is dirty on the screen (in need of repaint) // and what is supposed to be visible on the screen (the viewport) is what // we need to walk through line-by-line and repaint onto the screen. - const auto redraw = Viewport::Intersect(dirty, view); + const auto redraw = Viewport::Intersect(dirty, _viewport); // Retrieve the text buffer so we can read information out of it. auto& buffer = _pData->GetTextBuffer(); @@ -804,7 +1117,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) // For example, the screen might say we need to paint line 1 because it is dirty but the viewport // is actually looking at line 26 relative to the buffer. This means that we need line 27 out // of the backing buffer to fill in line 1 of the screen. - const auto screenPosition = bufferLine.Origin() - til::point{ 0, view.Top() }; + const auto screenPosition = bufferLine.Origin() - til::point{ 0, _viewport.Top() }; // Retrieve the cell information iterator limited to just this line we want to redraw. auto it = buffer.GetCellDataAt(bufferLine.Origin(), bufferLine); @@ -817,7 +1130,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) (bufferLine.RightExclusive() == buffer.GetSize().Width()); // Prepare the appropriate line transform for the current row and viewport offset. - LOG_IF_FAILED(pEngine->PrepareLineTransform(lineRendition, screenPosition.y, view.Left())); + LOG_IF_FAILED(pEngine->PrepareLineTransform(lineRendition, screenPosition.y, _viewport.Left())); // Ask the helper to paint through this specific line. _PaintBufferOutputHelper(pEngine, it, screenPosition, lineWrapped); @@ -826,7 +1139,7 @@ void Renderer::_PaintBufferOutput(_In_ IRenderEngine* const pEngine) const auto imageSlice = buffer.GetRowByOffset(row).GetImageSlice(); if (imageSlice) [[unlikely]] { - LOG_IF_FAILED(pEngine->PaintImageSlice(*imageSlice, screenPosition.y, view.Left())); + LOG_IF_FAILED(pEngine->PaintImageSlice(*imageSlice, screenPosition.y, _viewport.Left())); } } } @@ -1119,8 +1432,10 @@ bool Renderer::_isInHoveredInterval(const til::point coordTarget) const noexcept // - nullopt if the cursor is off or out-of-frame; otherwise, a CursorOptions void Renderer::_updateCursorInfo() { - // Get cursor position in buffer - auto coordCursor = _pData->GetCursorPosition(); + const auto& buffer = _pData->GetTextBuffer(); + const auto& cursor = buffer.GetCursor(); + const auto cursorPosition = cursor.GetPosition(); + auto coordCursor = cursorPosition; // Later this will be viewport-relative // GH#3166: Only draw the cursor if it's actually in the viewport. It // might be on the line that's in that partially visible row at the @@ -1130,12 +1445,12 @@ void Renderer::_updateCursorInfo() // The cursor is never rendered as double height, so we don't care about // the exact line rendition - only whether it's double width or not. - const auto doubleWidth = _pData->GetTextBuffer().IsDoubleWidthLine(coordCursor.y); + const auto doubleWidth = buffer.IsDoubleWidthLine(coordCursor.y); const auto lineRendition = doubleWidth ? LineRendition::DoubleWidth : LineRendition::SingleWidth; // We need to convert the screen coordinates of the viewport to an // equivalent range of buffer cells, taking line rendition into account. - const auto viewport = _pData->GetViewport().ToInclusive(); + const auto viewport = _viewport.ToInclusive(); const auto view = ScreenToBufferLine(viewport, lineRendition); // Note that we allow the X coordinate to be outside the left border by 1 position, @@ -1150,17 +1465,80 @@ void Renderer::_updateCursorInfo() const auto cursorColor = _renderSettings.GetColorTableEntry(TextColor::CURSOR_COLOR); const auto useColor = cursorColor != INVALID_COLOR; + // Update inhibitors based on whatever the VT parser (= client app) wants. + AllowCursorVisibility(InhibitionSource::Client, cursor.IsVisible()); + AllowCursorBlinking(InhibitionSource::Client, cursor.IsBlinking()); + + // If the buffer or cursor changed, turn the cursor on for the next cycle. This makes it + // so that rapidly typing/printing keeps the cursor on continuously, which looks nicer. + { + const auto cursorBufferMutationId = buffer.GetLastMutationId(); + const auto cursorCursorMutationId = cursor.GetLastMutationId(); + + if (_cursorBufferMutationId != cursorBufferMutationId || _cursorCursorMutationId != cursorCursorMutationId) + { + _cursorBufferMutationId = cursorBufferMutationId; + _cursorCursorMutationId = cursorCursorMutationId; + _cursorBlinkerOn = true; + + // We'll restart the timer below if there are no inhibitors. + StopTimer(_cursorBlinker); + } + } + + if (_cursorVisibilityInhibitors.any() || _cursorBlinkingInhibitors.any()) + { + StopTimer(_cursorBlinker); + } + else if (!IsTimerRunning(_cursorBlinker)) + { + const auto actual = GetTimerInterval(_cursorBlinker); + auto expected = _pData->GetBlinkInterval(); + + if (expected > TimerDuration::zero() && expected < TimerDuration::max()) + { + if (expected != actual) + { + StartRepeatingTimer(_cursorBlinker, expected); + } + } + else + { + // If blinking is disabled due to the OS settings, then we force-enable it. + _cursorBlinkerOn = true; + } + } + + // If blinking is disabled, the cursor is always on. + _cursorBlinkerOn |= _cursorBlinkingInhibitors.any(); + + auto cursorHeight = cursor.GetSize(); + // Now adjust the height for the overwrite/insert mode. If we're in overwrite mode, IsDouble will be set. + // When IsDouble is set, we either need to double the height of the cursor, or if it's already too big, + // then we need to shrink it by half. + if (cursor.IsDouble()) + { + if (cursorHeight > 50) // 50 because 50 percent is half of 100 percent which is the max size. + { + cursorHeight >>= 1; + } + else + { + cursorHeight <<= 1; + } + } + _currentCursorOptions.coordCursor = coordCursor; _currentCursorOptions.viewportLeft = viewport.left; _currentCursorOptions.lineRendition = lineRendition; - _currentCursorOptions.ulCursorHeightPercent = _pData->GetCursorHeight(); + _currentCursorOptions.ulCursorHeightPercent = cursorHeight; _currentCursorOptions.cursorPixelWidth = _pData->GetCursorPixelWidth(); - _currentCursorOptions.fIsDoubleWidth = _pData->IsCursorDoubleWidth(); - _currentCursorOptions.cursorType = _pData->GetCursorStyle(); + _currentCursorOptions.fIsDoubleWidth = buffer.GetRowByOffset(cursorPosition.y).DbcsAttrAt(cursorPosition.x) != DbcsAttribute::Single; + _currentCursorOptions.cursorType = cursor.GetType(); _currentCursorOptions.fUseColor = useColor; _currentCursorOptions.cursorColor = cursorColor; - _currentCursorOptions.isVisible = _pData->IsCursorVisible(); - _currentCursorOptions.isOn = _currentCursorOptions.isVisible && _pData->IsCursorOn(); + _currentCursorOptions.isVisible = !_cursorVisibilityInhibitors.any(); + _currentCursorOptions.isOn = _currentCursorOptions.isVisible && _cursorBlinkerOn; _currentCursorOptions.inViewport = xInRange && yInRange; } @@ -1184,7 +1562,7 @@ void Renderer::_invalidateCurrentCursor() const if (view.TrimToViewport(&rect)) { - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->InvalidateCursor(&rect)); } @@ -1194,9 +1572,16 @@ void Renderer::_invalidateCurrentCursor() const // If we had previously drawn a composition at the previous cursor position // we need to invalidate the entire line because who knows what changed. // (It's possible to figure that out, but not worth the effort right now.) -void Renderer::_invalidateOldComposition() const +void Renderer::_invalidateOldComposition() { - if (!_compositionCache || !_currentCursorOptions.inViewport) + if (!_compositionCache) + { + return; + } + + _compositionCache.reset(); + + if (!_currentCursorOptions.inViewport) { return; } @@ -1208,7 +1593,7 @@ void Renderer::_invalidateOldComposition() const til::rect rect{ 0, coord.y, til::CoordTypeMax, coord.y + 1 }; if (view.TrimToViewport(&rect)) { - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->Invalidate(&rect)); } @@ -1224,20 +1609,19 @@ void Renderer::_prepareNewComposition() return; } - const auto viewport = _pData->GetViewport(); - const auto coordCursor = _pData->GetCursorPosition(); + auto& buffer = _pData->GetTextBuffer(); + const auto coordCursor = buffer.GetCursor().GetPosition(); til::rect line{ 0, coordCursor.y, til::CoordTypeMax, coordCursor.y + 1 }; - if (viewport.TrimToViewport(&line)) + if (_viewport.TrimToViewport(&line)) { - viewport.ConvertToOrigin(&line); + _viewport.ConvertToOrigin(&line); - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { LOG_IF_FAILED(pEngine->Invalidate(&line)); } - auto& buffer = _pData->GetTextBuffer(); auto& scratch = buffer.GetScratchpadRow(); const auto& activeComposition = _pData->GetActiveComposition(); @@ -1399,32 +1783,17 @@ void Renderer::_ScrollPreviousSelection(const til::point delta) void Renderer::AddRenderEngine(_In_ IRenderEngine* const pEngine) { THROW_HR_IF_NULL(E_INVALIDARG, pEngine); - - for (auto& p : _engines) - { - if (!p) - { - p = pEngine; - _forceUpdateViewport = true; - return; - } - } - - THROW_HR_MSG(E_UNEXPECTED, "engines array is full"); + _engines.push_back(pEngine); + _forceUpdateViewport = true; } void Renderer::RemoveRenderEngine(_In_ IRenderEngine* const pEngine) { THROW_HR_IF_NULL(E_INVALIDARG, pEngine); - for (auto& p : _engines) - { - if (p == pEngine) - { - p = nullptr; - return; - } - } + std::erase_if(_engines, [=](IRenderEngine* e) { + return pEngine == e; + }); } // Method Description: @@ -1464,7 +1833,7 @@ void Renderer::SetRendererEnteredErrorStateCallback(std::function pfn) void Renderer::UpdateHyperlinkHoveredId(uint16_t id) noexcept { _hyperlinkHoveredId = id; - FOREACH_ENGINE(pEngine) + for (const auto pEngine : _engines) { pEngine->UpdateHyperlinkHoveredId(id); } @@ -1474,13 +1843,3 @@ void Renderer::UpdateLastHoveredInterval(const std::optionalWaitUntilCanRender(); - } -} diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 31acc728603..1cd157957ca 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -1,41 +1,40 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- Renderer.hpp - -Abstract: -- This is the definition of our renderer. -- It provides interfaces for the console application to notify when various portions of the console state have changed and need to be redrawn. -- It requires a data interface to fetch relevant console structures required for drawing and a drawing engine target for output. - -Author(s): -- Michael Niksa (MiNiksa) 17-Nov-2015 ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once +#include "../../buffer/out/textBuffer.hpp" #include "../inc/IRenderEngine.hpp" #include "../inc/RenderSettings.hpp" -#include "thread.hpp" - -#include "../../buffer/out/textBuffer.hpp" - namespace Microsoft::Console::Render { + enum class InhibitionSource + { + Client, // E.g. VT sequences + Host, // E.g. because the window is out of focus + User, // The user turned it off + }; + class Renderer { public: Renderer(RenderSettings& renderSettings, IRenderData* pData); + ~Renderer(); IRenderData* GetRenderData() const noexcept; - [[nodiscard]] HRESULT PaintFrame(); + TimerHandle RegisterTimer(const char* description, TimerCallback routine); + bool IsTimerRunning(TimerHandle handle) const; + TimerDuration GetTimerInterval(TimerHandle handle) const; + void StarTimer(TimerHandle handle, TimerDuration delay); + void StartRepeatingTimer(TimerHandle handle, TimerDuration interval); + void StopTimer(TimerHandle handle); void NotifyPaintFrame() noexcept; void SynchronizedOutputChanged() noexcept; + void AllowCursorVisibility(InhibitionSource source, bool enable) noexcept; + void AllowCursorBlinking(InhibitionSource source, bool enable) noexcept; void TriggerSystemRedraw(const til::rect* const prcDirtyClient); void TriggerRedraw(const Microsoft::Console::Types::Viewport& region); void TriggerRedraw(const til::point* const pcoord); @@ -66,7 +65,6 @@ namespace Microsoft::Console::Render bool IsGlyphWideByFont(const std::wstring_view glyph); void EnablePainting(); - void WaitUntilCanRender(); void AddRenderEngine(_In_ IRenderEngine* const pEngine); void RemoveRenderEngine(_In_ IRenderEngine* const pEngine); @@ -79,6 +77,14 @@ namespace Microsoft::Console::Render void UpdateLastHoveredInterval(const std::optional::interval>& newInterval); private: + struct TimerRoutine + { + const char* description = nullptr; + TimerRepr interval = 0; // Timers with a 0 interval are marked for deletion. + TimerRepr next = 0; + TimerCallback routine; + }; + // Caches some essential information about the active composition. // This allows us to properly invalidate it between frames, etc. struct CompositionCache @@ -90,10 +96,29 @@ namespace Microsoft::Console::Render static GridLineSet s_GetGridlines(const TextAttribute& textAttribute) noexcept; static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar); + // Base rendering loop + static DWORD s_renderThread(void*) noexcept; + DWORD _renderThread() noexcept; + void _waitUntilCanRender() noexcept; + + // Timer handling + void _starTimer(TimerHandle handle, TimerRepr delay, TimerRepr interval); + DWORD _calculateTimerMaxWait() noexcept; + void _waitUntilTimerOrRedraw() noexcept; + void _tickTimers() noexcept; + static TimerRepr _timerInstant() noexcept; + static TimerRepr _timerSaturatingAdd(TimerRepr a, TimerRepr b) noexcept; + static TimerRepr _timerSaturatingSub(TimerRepr a, TimerRepr b) noexcept; + static DWORD _timerToMillis(TimerRepr t) noexcept; + + // Actual rendering + [[nodiscard]] HRESULT PaintFrame(); [[nodiscard]] HRESULT _PaintFrame() noexcept; [[nodiscard]] HRESULT _PaintFrameForEngine(_In_ IRenderEngine* const pEngine) noexcept; + void _disablePainting() noexcept; void _synchronizeWithOutput() noexcept; bool _CheckViewportAndScroll(); + void _scheduleRenditionBlink(); [[nodiscard]] HRESULT _PaintBackground(_In_ IRenderEngine* const pEngine); void _PaintBufferOutput(_In_ IRenderEngine* const pEngine); void _PaintBufferOutputHelper(_In_ IRenderEngine* const pEngine, TextBufferCellIterator it, const til::point target, const bool lineWrapped); @@ -108,19 +133,41 @@ namespace Microsoft::Console::Render bool _isInHoveredInterval(til::point coordTarget) const noexcept; void _updateCursorInfo(); void _invalidateCurrentCursor() const; - void _invalidateOldComposition() const; + void _invalidateOldComposition(); void _prepareNewComposition(); [[nodiscard]] HRESULT _PrepareRenderInfo(_In_ IRenderEngine* const pEngine); + // Constructor parameters, weakly referenced RenderSettings& _renderSettings; - std::array _engines{}; IRenderData* _pData = nullptr; // Non-ownership pointer + + // Base render loop & timer management + wil::srwlock _threadMutex; + wil::unique_handle _thread; + wil::slim_event_manual_reset _enable; + std::atomic _redraw; + std::atomic _threadKeepRunning{ false }; + til::small_vector _engines; + til::small_vector _timers; + size_t _nextTimerId = 0; + static constexpr size_t _firstSoftFontChar = 0xEF20; size_t _lastSoftFontChar = 0; + uint16_t _hyperlinkHoveredId = 0; std::optional::interval> _hoveredInterval; - Microsoft::Console::Types::Viewport _viewport; + CursorOptions _currentCursorOptions{}; + TimerHandle _cursorBlinker; + uint64_t _cursorBufferMutationId = 0; + uint64_t _cursorCursorMutationId = 0; // Stupid name, but it's _cursor related and stores the cursor mutation id. + til::enumset _cursorVisibilityInhibitors; + til::enumset _cursorBlinkingInhibitors; + bool _cursorBlinkerOn = false; + + TimerHandle _renditionBlinker; + + Microsoft::Console::Types::Viewport _viewport; std::optional _compositionCache; std::vector _clusterBuffer; std::function _pfnBackgroundColorChanged; @@ -132,9 +179,5 @@ namespace Microsoft::Console::Render til::point_span _lastSelectionPaintSpan{}; size_t _lastSelectionPaintSize{}; std::vector _lastSelectionRectsByViewport{}; - - // Ordered last, so that it gets destroyed first. - // This ensures that the render thread stops accessing us. - RenderThread _thread{ this }; }; } diff --git a/src/renderer/inc/IRenderData.hpp b/src/renderer/inc/IRenderData.hpp index c2923df0ccf..6c70abe0ebb 100644 --- a/src/renderer/inc/IRenderData.hpp +++ b/src/renderer/inc/IRenderData.hpp @@ -1,16 +1,5 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- IRenderData.hpp - -Abstract: -- This serves as the interface defining all information needed to render to the screen. - -Author(s): -- Michael Niksa (MiNiksa) 17-Nov-2015 ---*/ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. #pragma once @@ -23,6 +12,8 @@ class TextBuffer; namespace Microsoft::Console::Render { + class Renderer; + struct CompositionRange { size_t len; // The number of chars in Composition::text that this .attr applies to @@ -36,6 +27,21 @@ namespace Microsoft::Console::Render size_t cursorPos = 0; }; + // Technically this entire block of definitions is specific to the Renderer class, + // but defining it here allows us to use the TimerDuration definition for IRenderData. + struct TimerHandle + { + explicit operator bool() const noexcept + { + return id != SIZE_T_MAX; + } + + size_t id = SIZE_T_MAX; + }; + using TimerRepr = ULONGLONG; + using TimerDuration = std::chrono::duration>; + using TimerCallback = std::function; + class IRenderData { public: @@ -53,28 +59,23 @@ namespace Microsoft::Console::Render virtual void UnlockConsole() noexcept = 0; // This block used to be the original IRenderData. - virtual til::point GetCursorPosition() const noexcept = 0; - virtual bool IsCursorVisible() const noexcept = 0; - virtual bool IsCursorOn() const noexcept = 0; - virtual ULONG GetCursorHeight() const noexcept = 0; - virtual CursorType GetCursorStyle() const noexcept = 0; + virtual TimerDuration GetBlinkInterval() noexcept = 0; // Return ::zero() or ::max() for no blink. virtual ULONG GetCursorPixelWidth() const noexcept = 0; - virtual bool IsCursorDoubleWidth() const = 0; - virtual const bool IsGridLineDrawingAllowed() noexcept = 0; - virtual const std::wstring_view GetConsoleTitle() const noexcept = 0; - virtual const std::wstring GetHyperlinkUri(uint16_t id) const = 0; - virtual const std::wstring GetHyperlinkCustomId(uint16_t id) const = 0; - virtual const std::vector GetPatternId(const til::point location) const = 0; + virtual bool IsGridLineDrawingAllowed() noexcept = 0; + virtual std::wstring_view GetConsoleTitle() const noexcept = 0; + virtual std::wstring GetHyperlinkUri(uint16_t id) const = 0; + virtual std::wstring GetHyperlinkCustomId(uint16_t id) const = 0; + virtual std::vector GetPatternId(const til::point location) const = 0; // This block used to be IUiaData. virtual std::pair GetAttributeColors(const TextAttribute& attr) const noexcept = 0; - virtual const bool IsSelectionActive() const = 0; - virtual const bool IsBlockSelection() const = 0; + virtual bool IsSelectionActive() const = 0; + virtual bool IsBlockSelection() const = 0; virtual void ClearSelection() = 0; virtual void SelectNewRegion(const til::point coordStart, const til::point coordEnd) = 0; - virtual const til::point GetSelectionAnchor() const noexcept = 0; - virtual const til::point GetSelectionEnd() const noexcept = 0; - virtual const bool IsUiaDataInitialized() const noexcept = 0; + virtual til::point GetSelectionAnchor() const noexcept = 0; + virtual til::point GetSelectionEnd() const noexcept = 0; + virtual bool IsUiaDataInitialized() const noexcept = 0; // Ideally this would not be stored on an interface, however ideally IRenderData should not be an interface in the first place. // This is because we should have only 1 way how to represent render data across the codebase anyway, and it should diff --git a/src/renderer/inc/RenderSettings.hpp b/src/renderer/inc/RenderSettings.hpp index 8b0d64379f7..2b25f01e0aa 100644 --- a/src/renderer/inc/RenderSettings.hpp +++ b/src/renderer/inc/RenderSettings.hpp @@ -20,7 +20,6 @@ namespace Microsoft::Console::Render public: enum class Mode : size_t { - BlinkAllowed, IndexedDistinguishableColors, AlwaysDistinguishableColors, IntenseIsBold, @@ -30,6 +29,7 @@ namespace Microsoft::Console::Render }; RenderSettings() noexcept; + void SaveDefaultSettings() noexcept; void RestoreDefaultSettings() noexcept; void SetRenderMode(const Mode mode, const bool enabled) noexcept; @@ -48,16 +48,14 @@ namespace Microsoft::Console::Render std::pair GetAttributeColors(const TextAttribute& attr) const noexcept; std::pair GetAttributeColorsWithAlpha(const TextAttribute& attr) const noexcept; COLORREF GetAttributeUnderlineColor(const TextAttribute& attr) const noexcept; - void ToggleBlinkRendition(class Renderer* renderer) noexcept; + void ToggleBlinkRendition() noexcept; private: - til::enumset _renderMode{ Mode::BlinkAllowed, Mode::IntenseIsBright }; + til::enumset _renderMode{ Mode::IntenseIsBright }; std::array _colorTable; std::array(ColorAlias::ENUM_COUNT)> _colorAliasIndices; std::array _defaultColorTable; std::array(ColorAlias::ENUM_COUNT)> _defaultColorAliasIndices; - size_t _blinkCycle = 0; - mutable bool _blinkIsInUse = false; bool _blinkShouldBeFaint = false; }; } diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 998db077169..7c1075a5943 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -110,9 +110,6 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) lineWidth = std::min(lineWidth, rightMargin + 1); } - // Turn off the cursor until we're done, so it isn't refreshed unnecessarily. - cursor.SetIsOn(false); - RowWriteState state{ .text = string, .columnLimit = lineWidth, @@ -120,9 +117,8 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) while (!state.text.empty()) { - if (cursor.IsDelayedEOLWrap() && wrapAtEOL) + if (const auto delayedCursorPosition = cursor.GetDelayEOLWrap(); delayedCursorPosition && wrapAtEOL) { - const auto delayedCursorPosition = cursor.GetDelayedAtPosition(); cursor.ResetDelayEOLWrap(); // Only act on a delayed EOL if we didn't move the cursor to a // different position from where the EOL was marked. @@ -192,8 +188,6 @@ void AdaptDispatch::_WriteToBuffer(const std::wstring_view string) } } - _ApplyCursorMovementFlags(cursor); - // Notify terminal and UIA of new text. // It's important to do this here instead of in TextBuffer, because here you // have access to the entire line of text, whereas TextBuffer writes it one @@ -400,22 +394,6 @@ void AdaptDispatch::_CursorMovePosition(const Offset rowOffset, const Offset col // Finally, attempt to set the adjusted cursor position back into the console. cursor.SetPosition(page.Buffer().ClampPositionWithinLine({ col, row })); - _ApplyCursorMovementFlags(cursor); -} - -// Routine Description: -// - Helper method which applies a bunch of flags that are typically set whenever -// the cursor is moved. The IsOn flag is set to true, and the Delay flag to false, -// to force a blinking cursor to be visible, so the user can immediately see the -// new position. -// Arguments: -// - cursor - The cursor instance to be updated -// Return Value: -// - -void AdaptDispatch::_ApplyCursorMovementFlags(Cursor& cursor) noexcept -{ - cursor.SetDelay(false); - cursor.SetIsOn(true); } // Routine Description: @@ -494,7 +472,7 @@ void AdaptDispatch::CursorSaveState() savedCursorState.Column = cursorPosition.x + 1; savedCursorState.Row = cursorPosition.y + 1; savedCursorState.Page = page.Number(); - savedCursorState.IsDelayedEOLWrap = page.Cursor().IsDelayedEOLWrap(); + savedCursorState.IsDelayedEOLWrap = page.Cursor().GetDelayEOLWrap().has_value(); savedCursorState.IsOriginModeRelative = _modes.test(Mode::Origin); savedCursorState.Attributes = page.Attributes(); savedCursorState.TermOutput = _termOutput; @@ -1825,7 +1803,7 @@ void AdaptDispatch::_ModeParamsHelper(const DispatchTypes::ModeParams param, con _terminalInput.SetInputMode(TerminalInput::Mode::AutoRepeat, enable); break; case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - _pages.ActivePage().Cursor().SetBlinkingAllowed(enable); + _pages.ActivePage().Cursor().SetIsBlinking(enable); break; case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: _pages.ActivePage().Cursor().SetIsVisible(enable); @@ -1990,7 +1968,7 @@ void AdaptDispatch::RequestMode(const DispatchTypes::ModeParams param) state = mapTemp(_terminalInput.GetInputMode(TerminalInput::Mode::AutoRepeat)); break; case DispatchTypes::ModeParams::ATT610_StartCursorBlink: - state = mapTemp(_pages.ActivePage().Cursor().IsBlinkingAllowed()); + state = mapTemp(_pages.ActivePage().Cursor().IsBlinking()); break; case DispatchTypes::ModeParams::DECTCEM_TextCursorEnableMode: state = mapTemp(_pages.ActivePage().Cursor().IsVisible()); @@ -2099,7 +2077,6 @@ void AdaptDispatch::_InsertDeleteLineHelper(const VTInt delta) // The IL and DL controls are also expected to move the cursor to the left margin. cursor.SetXPosition(leftMargin); - _ApplyCursorMovementFlags(cursor); } } @@ -2460,7 +2437,6 @@ bool AdaptDispatch::_DoLineFeed(const Page& page, const bool withReturn, const b // We trigger a scroll rather than a redraw, since that's more efficient, // but we need to turn the cursor off before doing so; otherwise, a ghost // cursor can be left behind in the previous position. - cursor.SetIsOn(false); textBuffer.TriggerScroll({ 0, -1 }); // And again, if the bottom margin didn't cover the full page, we @@ -2472,7 +2448,6 @@ bool AdaptDispatch::_DoLineFeed(const Page& page, const bool withReturn, const b } cursor.SetPosition(newPosition); - _ApplyCursorMovementFlags(cursor); return viewportMoved; } @@ -2524,7 +2499,6 @@ void AdaptDispatch::ReverseLineFeed() { // 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); } } @@ -2550,7 +2524,6 @@ void AdaptDispatch::BackIndex() else if (cursorPosition.x > 0) { cursor.SetXPosition(cursorPosition.x - 1); - _ApplyCursorMovementFlags(cursor); } } @@ -2576,7 +2549,6 @@ void AdaptDispatch::ForwardIndex() else if (cursorPosition.x < page.Buffer().GetLineWidth(cursorPosition.y) - 1) { cursor.SetXPosition(cursorPosition.x + 1); - _ApplyCursorMovementFlags(cursor); } } @@ -2639,9 +2611,8 @@ void AdaptDispatch::ForwardTab(const VTInt numTabs) // approach (i.e. they don't reset). For us this is a bit messy, since all // cursor movement resets the flag automatically, so we need to save the // original state here, and potentially reapply it after the move. - const auto delayedWrapOriginallySet = cursor.IsDelayedEOLWrap(); + const auto delayedWrapOriginallySet = cursor.GetDelayEOLWrap().has_value(); cursor.SetXPosition(column); - _ApplyCursorMovementFlags(cursor); if (delayedWrapOriginallySet) { cursor.DelayEOLWrap(); @@ -2678,7 +2649,6 @@ void AdaptDispatch::BackwardsTab(const VTInt numTabs) } cursor.SetXPosition(column); - _ApplyCursorMovementFlags(cursor); } //Routine Description: @@ -3055,7 +3025,7 @@ void AdaptDispatch::HardReset() _api.SetSystemMode(ITerminalApi::Mode::BracketedPaste, false); // Restore cursor blinking mode. - _pages.ActivePage().Cursor().SetBlinkingAllowed(true); + _pages.ActivePage().Cursor().SetIsBlinking(true); // Delete all current tab stops and reapply TabSet(DispatchTypes::TabSetType::SetEvery8Columns); @@ -3240,7 +3210,7 @@ void AdaptDispatch::SetCursorStyle(const DispatchTypes::CursorStyle cursorStyle) auto& cursor = _pages.ActivePage().Cursor(); cursor.SetType(actualType); - cursor.SetBlinkingAllowed(fEnableBlinking); + cursor.SetIsBlinking(fEnableBlinking); } // Routine Description: @@ -4310,7 +4280,7 @@ void AdaptDispatch::_ReportDECSLRMSetting() void AdaptDispatch::_ReportDECSCUSRSetting() const { const auto& cursor = _pages.ActivePage().Cursor(); - const auto blinking = cursor.IsBlinkingAllowed(); + const auto blinking = cursor.IsBlinking(); // A valid response always starts with 1 $ r. This is followed by a // number from 1 to 6 representing the cursor style. The ' q' indicates // this is a DECSCUSR response. @@ -4488,7 +4458,7 @@ void AdaptDispatch::_ReportCursorInformation() flags += (_modes.test(Mode::Origin) ? 1 : 0); flags += (_termOutput.IsSingleShiftPending(2) ? 2 : 0); flags += (_termOutput.IsSingleShiftPending(3) ? 4 : 0); - flags += (cursor.IsDelayedEOLWrap() ? 8 : 0); + flags += (cursor.GetDelayEOLWrap().has_value() ? 8 : 0); // Character set designations. const auto leftSetNumber = _termOutput.GetLeftSetNumber(); diff --git a/src/terminal/adapter/adaptDispatch.hpp b/src/terminal/adapter/adaptDispatch.hpp index af726c0e01e..a193a17602e 100644 --- a/src/terminal/adapter/adaptDispatch.hpp +++ b/src/terminal/adapter/adaptDispatch.hpp @@ -241,7 +241,6 @@ namespace Microsoft::Console::VirtualTerminal std::pair _GetVerticalMargins(const Page& page, const bool absolute) noexcept; std::pair _GetHorizontalMargins(const til::CoordType bufferWidth) noexcept; void _CursorMovePosition(const Offset rowOffset, const Offset colOffset, const bool clampInMargins); - void _ApplyCursorMovementFlags(Cursor& cursor) noexcept; 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); From 1b712b299cdc431263e7e578731125d19834922c Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 23 Sep 2025 22:20:22 +0200 Subject: [PATCH 2/8] Fix tests? --- .../TerminalApiTest.cpp | 40 ++++++----------- src/host/ut_host/ScreenBufferTests.cpp | 43 ++++++++----------- src/host/ut_host/TextBufferTests.cpp | 8 ---- .../adapter/ut_adapter/adapterTest.cpp | 26 +++++------ 4 files changed, 43 insertions(+), 74 deletions(-) diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp index 157bad1805d..4f720d1c7b6 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp @@ -139,29 +139,24 @@ void TerminalApiTest::CursorVisibility() term.Create({ 100, 100 }, 0, renderer); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsVisible()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); + VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); term.SetCursorOn(false); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsVisible()); - VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsOn()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); + VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); term.SetCursorOn(true); VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsVisible()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsOn()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); + VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); 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()); + VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); term.SetCursorOn(false); VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsVisible()); - VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsOn()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinkingAllowed()); + VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); } void TerminalApiTest::CursorVisibilityViaStateMachine() @@ -176,44 +171,35 @@ void TerminalApiTest::CursorVisibilityViaStateMachine() auto& cursor = tbi.GetCursor(); stateMachine.ProcessString(L"Hello World"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12l"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_FALSE(cursor.IsBlinkingAllowed()); + VERIFY_IS_FALSE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12h"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); - cursor.SetIsOn(false); stateMachine.ProcessString(L"\x1b[?12l"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_FALSE(cursor.IsBlinkingAllowed()); + VERIFY_IS_FALSE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12h"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?25l"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_FALSE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?25h"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12;25l"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_FALSE(cursor.IsBlinkingAllowed()); + VERIFY_IS_FALSE(cursor.IsBlinking()); VERIFY_IS_FALSE(cursor.IsVisible()); } diff --git a/src/host/ut_host/ScreenBufferTests.cpp b/src/host/ut_host/ScreenBufferTests.cpp index a3c6a20ad11..883e8f2095f 100644 --- a/src/host/ut_host/ScreenBufferTests.cpp +++ b/src/host/ut_host/ScreenBufferTests.cpp @@ -408,7 +408,7 @@ void ScreenBufferTests::AlternateBufferCursorInheritanceTest() mainCursor.SetPosition(mainCursorPos); mainCursor.SetIsVisible(mainCursorVisible); mainCursor.SetStyle(mainCursorSize, mainCursorType); - mainCursor.SetBlinkingAllowed(mainCursorBlinking); + mainCursor.SetIsBlinking(mainCursorBlinking); Log::Comment(L"Switch to the alternate buffer."); VERIFY_SUCCEEDED(mainBuffer.UseAlternateScreenBuffer({})); @@ -423,7 +423,7 @@ void ScreenBufferTests::AlternateBufferCursorInheritanceTest() Log::Comment(L"Confirm the cursor style is inherited from the main buffer."); VERIFY_ARE_EQUAL(mainCursorSize, altCursor.GetSize()); VERIFY_ARE_EQUAL(mainCursorType, altCursor.GetType()); - VERIFY_ARE_EQUAL(mainCursorBlinking, altCursor.IsBlinkingAllowed()); + VERIFY_ARE_EQUAL(mainCursorBlinking, altCursor.IsBlinking()); Log::Comment(L"Set the cursor attributes in the alt buffer."); auto altCursorPos = til::point{ 5, 3 }; @@ -434,7 +434,7 @@ void ScreenBufferTests::AlternateBufferCursorInheritanceTest() altCursor.SetPosition(altCursorPos); altCursor.SetIsVisible(altCursorVisible); altCursor.SetStyle(altCursorSize, altCursorType); - altCursor.SetBlinkingAllowed(altCursorBlinking); + altCursor.SetIsBlinking(altCursorBlinking); Log::Comment(L"Switch back to the main buffer."); useMain.release(); @@ -448,7 +448,7 @@ void ScreenBufferTests::AlternateBufferCursorInheritanceTest() Log::Comment(L"Confirm the cursor style is inherited from the alt buffer."); VERIFY_ARE_EQUAL(altCursorSize, mainCursor.GetSize()); VERIFY_ARE_EQUAL(altCursorType, mainCursor.GetType()); - VERIFY_ARE_EQUAL(altCursorBlinking, mainCursor.IsBlinkingAllowed()); + VERIFY_ARE_EQUAL(altCursorBlinking, mainCursor.IsBlinking()); } void ScreenBufferTests::TestReverseLineFeed() @@ -6890,7 +6890,7 @@ void ScreenBufferTests::CursorSaveRestore() stateMachine.ProcessString(restoreCursor); // Verify initial position, delayed wrap, colors, and graphic character set. VERIFY_ARE_EQUAL(til::point(20, 10), cursor.GetPosition()); - VERIFY_IS_TRUE(cursor.IsDelayedEOLWrap()); + VERIFY_IS_TRUE(cursor.GetDelayEOLWrap().has_value()); cursor.ResetDelayEOLWrap(); VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes()); stateMachine.ProcessString(asciiText); @@ -6905,7 +6905,7 @@ void ScreenBufferTests::CursorSaveRestore() stateMachine.ProcessString(restoreCursor); // Verify initial saved position, delayed wrap, colors, and graphic character set. VERIFY_ARE_EQUAL(til::point(20, 10), cursor.GetPosition()); - VERIFY_IS_TRUE(cursor.IsDelayedEOLWrap()); + VERIFY_IS_TRUE(cursor.GetDelayEOLWrap().has_value()); cursor.ResetDelayEOLWrap(); VERIFY_ARE_EQUAL(colorAttrs, si.GetAttributes()); stateMachine.ProcessString(asciiText); @@ -6923,7 +6923,7 @@ void ScreenBufferTests::CursorSaveRestore() stateMachine.ProcessString(restoreCursor); // Verify home position, no delayed wrap, default attributes, and ascii character set. VERIFY_ARE_EQUAL(til::point(0, 0), cursor.GetPosition()); - VERIFY_IS_FALSE(cursor.IsDelayedEOLWrap()); + VERIFY_IS_FALSE(cursor.GetDelayEOLWrap().has_value()); VERIFY_ARE_EQUAL(defaultAttrs, si.GetAttributes()); stateMachine.ProcessString(asciiText); VERIFY_IS_TRUE(_ValidateLineContains(til::point(0, 0), asciiText, defaultAttrs)); @@ -7059,44 +7059,35 @@ void ScreenBufferTests::TestCursorIsOn() auto& cursor = tbi.GetCursor(); stateMachine.ProcessString(L"Hello World"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12l"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_FALSE(cursor.IsBlinkingAllowed()); + VERIFY_IS_FALSE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12h"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); - cursor.SetIsOn(false); stateMachine.ProcessString(L"\x1b[?12l"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_FALSE(cursor.IsBlinkingAllowed()); + VERIFY_IS_FALSE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12h"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?25l"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_FALSE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?25h"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_TRUE(cursor.IsBlinkingAllowed()); + VERIFY_IS_TRUE(cursor.IsBlinking()); VERIFY_IS_TRUE(cursor.IsVisible()); stateMachine.ProcessString(L"\x1b[?12;25l"); - VERIFY_IS_TRUE(cursor.IsOn()); - VERIFY_IS_FALSE(cursor.IsBlinkingAllowed()); + VERIFY_IS_FALSE(cursor.IsBlinking()); VERIFY_IS_FALSE(cursor.IsVisible()); } @@ -8158,7 +8149,7 @@ void ScreenBufferTests::DelayedWrapReset() stateMachine.ProcessCharacter(L'X'); { auto& cursor = si.GetTextBuffer().GetCursor(); - VERIFY_IS_TRUE(cursor.IsDelayedEOLWrap()); + VERIFY_IS_TRUE(cursor.GetDelayEOLWrap().has_value()); VERIFY_ARE_EQUAL(startPos, cursor.GetPosition()); } @@ -8170,7 +8161,7 @@ void ScreenBufferTests::DelayedWrapReset() { auto& cursor = si.GetTextBuffer().GetCursor(); const auto actualPos = cursor.GetPosition() - si.GetViewport().Origin(); - VERIFY_IS_FALSE(cursor.IsDelayedEOLWrap()); + VERIFY_IS_FALSE(cursor.GetDelayEOLWrap().has_value()); VERIFY_ARE_EQUAL(expectedPos, actualPos); } } diff --git a/src/host/ut_host/TextBufferTests.cpp b/src/host/ut_host/TextBufferTests.cpp index ac994cb79c6..1db72d02d31 100644 --- a/src/host/ut_host/TextBufferTests.cpp +++ b/src/host/ut_host/TextBufferTests.cpp @@ -366,23 +366,15 @@ void TextBufferTests::TestCopyProperties() testTextBuffer->GetCursor().SetIsVisible(false); otherTbi.GetCursor().SetIsVisible(true); - testTextBuffer->GetCursor().SetIsOn(false); - otherTbi.GetCursor().SetIsOn(true); - testTextBuffer->GetCursor().SetIsDouble(false); otherTbi.GetCursor().SetIsDouble(true); - testTextBuffer->GetCursor().SetDelay(false); - otherTbi.GetCursor().SetDelay(true); - // run copy testTextBuffer->CopyProperties(otherTbi); // test that new now contains values from other VERIFY_IS_TRUE(testTextBuffer->GetCursor().IsVisible()); - VERIFY_IS_TRUE(testTextBuffer->GetCursor().IsOn()); VERIFY_IS_TRUE(testTextBuffer->GetCursor().IsDouble()); - VERIFY_IS_TRUE(testTextBuffer->GetCursor().GetDelay()); } void TextBufferTests::TestLastNonSpace(const til::CoordType cursorPosY) diff --git a/src/terminal/adapter/ut_adapter/adapterTest.cpp b/src/terminal/adapter/ut_adapter/adapterTest.cpp index c6525ab8dd6..1f42da08f66 100644 --- a/src/terminal/adapter/ut_adapter/adapterTest.cpp +++ b/src/terminal/adapter/ut_adapter/adapterTest.cpp @@ -1977,49 +1977,49 @@ class AdapterTest Log::Comment(L"Requesting DECSCUSR style (blinking block)."); _testGetSet->PrepData(); - _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(true); + _testGetSet->_textBuffer->GetCursor().SetIsBlinking(true); _testGetSet->_textBuffer->GetCursor().SetType(CursorType::FullBox); requestSetting(L" q"); _testGetSet->ValidateInputEvent(L"\033P1$r1 q\033\\"); Log::Comment(L"Requesting DECSCUSR style (steady block)."); _testGetSet->PrepData(); - _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(false); + _testGetSet->_textBuffer->GetCursor().SetIsBlinking(false); _testGetSet->_textBuffer->GetCursor().SetType(CursorType::FullBox); requestSetting(L" q"); _testGetSet->ValidateInputEvent(L"\033P1$r2 q\033\\"); Log::Comment(L"Requesting DECSCUSR style (blinking underline)."); _testGetSet->PrepData(); - _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(true); + _testGetSet->_textBuffer->GetCursor().SetIsBlinking(true); _testGetSet->_textBuffer->GetCursor().SetType(CursorType::Underscore); requestSetting(L" q"); _testGetSet->ValidateInputEvent(L"\033P1$r3 q\033\\"); Log::Comment(L"Requesting DECSCUSR style (steady underline)."); _testGetSet->PrepData(); - _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(false); + _testGetSet->_textBuffer->GetCursor().SetIsBlinking(false); _testGetSet->_textBuffer->GetCursor().SetType(CursorType::Underscore); requestSetting(L" q"); _testGetSet->ValidateInputEvent(L"\033P1$r4 q\033\\"); Log::Comment(L"Requesting DECSCUSR style (blinking bar)."); _testGetSet->PrepData(); - _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(true); + _testGetSet->_textBuffer->GetCursor().SetIsBlinking(true); _testGetSet->_textBuffer->GetCursor().SetType(CursorType::VerticalBar); requestSetting(L" q"); _testGetSet->ValidateInputEvent(L"\033P1$r5 q\033\\"); Log::Comment(L"Requesting DECSCUSR style (steady bar)."); _testGetSet->PrepData(); - _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(false); + _testGetSet->_textBuffer->GetCursor().SetIsBlinking(false); _testGetSet->_textBuffer->GetCursor().SetType(CursorType::VerticalBar); requestSetting(L" q"); _testGetSet->ValidateInputEvent(L"\033P1$r6 q\033\\"); Log::Comment(L"Requesting DECSCUSR style (non-standard)."); _testGetSet->PrepData(); - _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(true); + _testGetSet->_textBuffer->GetCursor().SetIsBlinking(true); _testGetSet->_textBuffer->GetCursor().SetType(CursorType::Legacy); requestSetting(L" q"); _testGetSet->ValidateInputEvent(L"\033P1$r0 q\033\\"); @@ -2814,12 +2814,12 @@ class AdapterTest VERIFY_IS_TRUE(_pDispatch->_modes.test(AdaptDispatch::Mode::Origin)); VERIFY_IS_FALSE(termOutput.IsSingleShiftPending(2)); VERIFY_IS_TRUE(termOutput.IsSingleShiftPending(3)); - VERIFY_IS_FALSE(textBuffer.GetCursor().IsDelayedEOLWrap()); + VERIFY_IS_FALSE(textBuffer.GetCursor().GetDelayEOLWrap().has_value()); stateMachine.ProcessString(L"\033P1$t1;1;1;@;@;J;0;2;@;BBBB\033\\"); VERIFY_IS_FALSE(_pDispatch->_modes.test(AdaptDispatch::Mode::Origin)); VERIFY_IS_TRUE(termOutput.IsSingleShiftPending(2)); VERIFY_IS_FALSE(termOutput.IsSingleShiftPending(3)); - VERIFY_IS_TRUE(textBuffer.GetCursor().IsDelayedEOLWrap()); + VERIFY_IS_TRUE(textBuffer.GetCursor().GetDelayEOLWrap().has_value()); Log::Comment(L"Restore charset configuration"); stateMachine.ProcessString(L"\033P1$t1;1;1;@;@;@;3;1;H;ABCF\033\\"); @@ -2895,15 +2895,15 @@ class AdapterTest // success cases // set blinking mode = true Log::Comment(L"Test 1: enable blinking = true"); - _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(false); + _testGetSet->_textBuffer->GetCursor().SetIsBlinking(false); _pDispatch->SetMode(DispatchTypes::ATT610_StartCursorBlink); - VERIFY_IS_TRUE(_testGetSet->_textBuffer->GetCursor().IsBlinkingAllowed()); + VERIFY_IS_TRUE(_testGetSet->_textBuffer->GetCursor().IsBlinking()); // set blinking mode = false Log::Comment(L"Test 2: enable blinking = false"); - _testGetSet->_textBuffer->GetCursor().SetBlinkingAllowed(true); + _testGetSet->_textBuffer->GetCursor().SetIsBlinking(true); _pDispatch->ResetMode(DispatchTypes::ATT610_StartCursorBlink); - VERIFY_IS_FALSE(_testGetSet->_textBuffer->GetCursor().IsBlinkingAllowed()); + VERIFY_IS_FALSE(_testGetSet->_textBuffer->GetCursor().IsBlinking()); } TEST_METHOD(ScrollMarginsTest) From 1c07227047b3f8616074b550615046dee8db223d Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 24 Sep 2025 14:53:46 +0200 Subject: [PATCH 3/8] Fix tests --- .../TerminalApiTest.cpp | 72 ------------------- src/host/AccessibilityNotifier.cpp | 8 +-- 2 files changed, 4 insertions(+), 76 deletions(-) diff --git a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp index 4f720d1c7b6..51e53f1738e 100644 --- a/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp +++ b/src/cascadia/UnitTests_TerminalCore/TerminalApiTest.cpp @@ -26,14 +26,12 @@ namespace TerminalCoreUnitTests TEST_METHOD(SetColorTableEntry); - TEST_METHOD(CursorVisibility); TEST_METHOD(CursorVisibilityViaStateMachine); // Terminal::_WriteBuffer used to enter infinite loops under certain conditions. // This test ensures that Terminal::_WriteBuffer doesn't get stuck when // PrintString() is called with more code units than the buffer width. TEST_METHOD(PrintStringOfSurrogatePairs); - TEST_METHOD(CheckDoubleWidthCursor); TEST_METHOD(AddHyperlink); TEST_METHOD(AddHyperlinkCustomId); @@ -131,34 +129,6 @@ void TerminalApiTest::PrintStringOfSurrogatePairs() return; } -void TerminalApiTest::CursorVisibility() -{ - // GH#3093 - Cursor Visibility and On states shouldn't affect each other - Terminal term{ Terminal::TestDummyMarker{} }; - DummyRenderer renderer{ &term }; - term.Create({ 100, 100 }, 0, renderer); - - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsVisible()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); - - term.SetCursorOn(false); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsVisible()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); - - term.SetCursorOn(true); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsVisible()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); - - auto& textBuffer = term.GetBufferAndViewport().buffer; - textBuffer.GetCursor().SetIsVisible(false); - VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsVisible()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); - - term.SetCursorOn(false); - VERIFY_IS_FALSE(term._mainBuffer->GetCursor().IsVisible()); - VERIFY_IS_TRUE(term._mainBuffer->GetCursor().IsBlinking()); -} - void TerminalApiTest::CursorVisibilityViaStateMachine() { // This is a nearly literal copy-paste of ScreenBufferTests::TestCursorIsOn, adapted for the Terminal @@ -203,48 +173,6 @@ void TerminalApiTest::CursorVisibilityViaStateMachine() VERIFY_IS_FALSE(cursor.IsVisible()); } -void TerminalApiTest::CheckDoubleWidthCursor() -{ - Terminal term{ Terminal::TestDummyMarker{} }; - DummyRenderer renderer{ &term }; - term.Create({ 100, 100 }, 0, renderer); - - auto& tbi = *(term._mainBuffer); - auto& stateMachine = *(term._stateMachine); - auto& cursor = tbi.GetCursor(); - - // Lets stuff the buffer with single width characters, - // but leave the last two columns empty for double width. - std::wstring singleWidthText; - singleWidthText.reserve(98); - for (size_t i = 0; i < 98; ++i) - { - singleWidthText.append(L"A"); - } - stateMachine.ProcessString(singleWidthText); - VERIFY_IS_TRUE(cursor.GetPosition().x == 98); - - // Stuff two double width characters. - std::wstring doubleWidthText{ L"我愛" }; - stateMachine.ProcessString(doubleWidthText); - - // The last 'A' - cursor.SetPosition({ 97, 0 }); - VERIFY_IS_FALSE(term.IsCursorDoubleWidth()); - - // This and the next CursorPos are taken up by '我‘ - cursor.SetPosition({ 98, 0 }); - VERIFY_IS_TRUE(term.IsCursorDoubleWidth()); - cursor.SetPosition({ 99, 0 }); - VERIFY_IS_TRUE(term.IsCursorDoubleWidth()); - - // This and the next CursorPos are taken up by ’愛‘ - cursor.SetPosition({ 0, 1 }); - VERIFY_IS_TRUE(term.IsCursorDoubleWidth()); - cursor.SetPosition({ 1, 1 }); - VERIFY_IS_TRUE(term.IsCursorDoubleWidth()); -} - void TerminalCoreUnitTests::TerminalApiTest::AddHyperlink() { // This is a nearly literal copy-paste of ScreenBufferTests::TestAddHyperlink, adapted for the Terminal diff --git a/src/host/AccessibilityNotifier.cpp b/src/host/AccessibilityNotifier.cpp index 60992f9d18c..fa1fa42411c 100644 --- a/src/host/AccessibilityNotifier.cpp +++ b/src/host/AccessibilityNotifier.cpp @@ -70,10 +70,6 @@ void AccessibilityNotifier::Initialize(HWND hwnd, DWORD msaaDelay, DWORD uiaDela void AccessibilityNotifier::SetUIAProvider(IRawElementProviderSimple* provider) noexcept { - // NOTE: The assumption is that you're holding the console lock when calling any of the member functions. - // This is why we can safely update these members (no worker thread is running nor can be scheduled). - assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); - // If UIA events are disabled, don't set _uiaProvider either. // It would trigger unnecessary work. if (!_uiaEnabled) @@ -81,6 +77,10 @@ void AccessibilityNotifier::SetUIAProvider(IRawElementProviderSimple* provider) return; } + // NOTE: The assumption is that you're holding the console lock when calling any of the member functions. + // This is why we can safely update these members (no worker thread is running nor can be scheduled). + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); + // Of course we must ensure our precious provider object doesn't go away. if (provider) { From 7083bc0b9ba48a76102a4e9efc604f2e359dd869 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 14 Oct 2025 20:11:55 +0200 Subject: [PATCH 4/8] Fix off-by-one bugs --- src/host/AccessibilityNotifier.cpp | 10 +++++++- src/host/AccessibilityNotifier.h | 1 + src/host/_output.cpp | 7 +++-- src/host/_stream.cpp | 38 ++++++++++++++++++++-------- src/host/lib/hostlib.vcxproj.filters | 3 +++ 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/host/AccessibilityNotifier.cpp b/src/host/AccessibilityNotifier.cpp index fa1fa42411c..bf7c47a3c69 100644 --- a/src/host/AccessibilityNotifier.cpp +++ b/src/host/AccessibilityNotifier.cpp @@ -167,10 +167,18 @@ void AccessibilityNotifier::SelectionChanged() noexcept } } +bool AccessibilityNotifier::WantsRegionChangedEvents() const noexcept +{ + // See RegionChanged(). + return (_msaaEnabled && IsWinEventHookInstalled(EVENT_CONSOLE_UPDATE_REGION)) || + (_uiaProvider.load(std::memory_order_relaxed) != nullptr); +} + // Emits EVENT_CONSOLE_UPDATE_REGION, the region of the console that changed. +// `end` is expected to be an inclusive coordinate. void AccessibilityNotifier::RegionChanged(til::point begin, til::point end) noexcept { - if (begin >= end) + if (begin > end) { return; } diff --git a/src/host/AccessibilityNotifier.h b/src/host/AccessibilityNotifier.h index 137241158cf..332a5994625 100644 --- a/src/host/AccessibilityNotifier.h +++ b/src/host/AccessibilityNotifier.h @@ -26,6 +26,7 @@ namespace Microsoft::Console void CursorChanged(til::point position, bool activeSelection) noexcept; void SelectionChanged() noexcept; + bool WantsRegionChangedEvents() const noexcept; void RegionChanged(til::point begin, til::point end) noexcept; void ScrollBuffer(til::CoordType delta) noexcept; void ScrollViewport(til::point delta) noexcept; diff --git a/src/host/_output.cpp b/src/host/_output.cpp index 83482c5eb4f..7850a9e4e4b 100644 --- a/src/host/_output.cpp +++ b/src/host/_output.cpp @@ -253,10 +253,13 @@ static FillConsoleResult FillConsoleImpl(SCREEN_INFORMATION& screenInfo, FillCon ImageSlice::EraseCells(screenInfo.GetTextBuffer(), startingCoordinate, result.cellsModified); } + if (result.cellsModified > 0) { - // Notify accessibility auto endingCoordinate = startingCoordinate; - bufferSize.WalkInBounds(endingCoordinate, result.cellsModified); + if (result.cellsModified > 1) + { + bufferSize.WalkInBounds(endingCoordinate, result.cellsModified - 1); + } auto& an = ServiceLocator::LocateGlobals().accessibilityNotifier; an.RegionChanged(startingCoordinate, endingCoordinate); diff --git a/src/host/_stream.cpp b/src/host/_stream.cpp index 93c5875047d..235ba52b10f 100644 --- a/src/host/_stream.cpp +++ b/src/host/_stream.cpp @@ -17,7 +17,6 @@ #include "VtIo.hpp" #include "../types/inc/convert.hpp" -#include "../types/inc/GlyphWidth.hpp" #include "../types/inc/Viewport.hpp" #include "../interactivity/inc/ServiceLocator.hpp" @@ -34,29 +33,46 @@ constexpr bool controlCharPredicate(wchar_t wch) static auto raiseAccessibilityEventsOnExit(SCREEN_INFORMATION& screenInfo) { const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& bufferBefore = gci.GetActiveOutputBuffer(); - const auto cursorBefore = bufferBefore.GetTextBuffer().GetCursor().GetPosition(); + const auto bufferBefore = &gci.GetActiveOutputBuffer(); + const auto cursorBefore = bufferBefore->GetTextBuffer().GetCursor().GetPosition(); - auto raise = wil::scope_exit([&bufferBefore, cursorBefore] { + auto raise = wil::scope_exit([bufferBefore, cursorBefore] { // !!! NOTE !!! `bufferBefore` may now be a stale pointer, because VT // sequences can switch between the main and alternative screen buffer. auto& an = ServiceLocator::LocateGlobals().accessibilityNotifier; const auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - const auto& bufferAfter = gci.GetActiveOutputBuffer(); - const auto cursorAfter = bufferAfter.GetTextBuffer().GetCursor().GetPosition(); + const auto& bufferAfter = &gci.GetActiveOutputBuffer(); + const auto cursorAfter = bufferAfter->GetTextBuffer().GetCursor().GetPosition(); - if (&bufferBefore == &bufferAfter) - { - an.RegionChanged(cursorBefore, cursorAfter); - } if (cursorBefore != cursorAfter) { + if (bufferBefore == bufferAfter && an.WantsRegionChangedEvents()) + { + // Make the range ordered... + auto beg = cursorBefore; + auto end = cursorAfter; + if (beg > end) + { + std::swap(beg, end); + } + + // ...and make it inclusive. + end.x--; + if (end.x < 0) + { + end.y--; + end.x = bufferAfter->GetBufferSize().Width() - 1; + } + + an.RegionChanged(beg, end); + } + an.CursorChanged(cursorAfter, false); } }); // Don't raise any events for inactive buffers. - if (&bufferBefore != &screenInfo) + if (bufferBefore != &screenInfo) { raise.release(); } diff --git a/src/host/lib/hostlib.vcxproj.filters b/src/host/lib/hostlib.vcxproj.filters index f149308c8a9..ff4ebc0ba74 100644 --- a/src/host/lib/hostlib.vcxproj.filters +++ b/src/host/lib/hostlib.vcxproj.filters @@ -144,6 +144,9 @@ Source Files + + Source Files + From 8214051f689d34abc0865e5cd44a17bf84877147 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Tue, 14 Oct 2025 20:12:50 +0200 Subject: [PATCH 5/8] Add support for EVENT_CONSOLE_UPDATE_SIMPLE --- src/host/AccessibilityNotifier.cpp | 228 ++++++++++++++++------------- src/host/AccessibilityNotifier.h | 2 +- src/host/selection.cpp | 4 - 3 files changed, 130 insertions(+), 104 deletions(-) diff --git a/src/host/AccessibilityNotifier.cpp b/src/host/AccessibilityNotifier.cpp index bf7c47a3c69..a2272240f7e 100644 --- a/src/host/AccessibilityNotifier.cpp +++ b/src/host/AccessibilityNotifier.cpp @@ -4,6 +4,7 @@ #include "precomp.h" #include "AccessibilityNotifier.h" +#include "../types/inc/convert.hpp" #include "../interactivity/inc/ServiceLocator.hpp" using namespace Microsoft::Console; @@ -90,7 +91,10 @@ void AccessibilityNotifier::SetUIAProvider(IRawElementProviderSimple* provider) const auto old = _uiaProvider.exchange(provider, std::memory_order_relaxed); // Before we can release the old object, we must ensure it's not in use by a worker thread. - WaitForThreadpoolTimerCallbacks(_timer.get(), TRUE); + if (_timer) + { + WaitForThreadpoolTimerCallbacks(_timer.get(), TRUE); + } if (old) { @@ -140,16 +144,26 @@ void AccessibilityNotifier::SetUIAProvider(IRawElementProviderSimple* provider) // Unfortunately there's no way to know whether anyone even needs this information so we always raise this. void AccessibilityNotifier::CursorChanged(til::point position, bool activeSelection) noexcept { + const auto uiaEnabled = _uiaProvider.load(std::memory_order_relaxed) != nullptr; + // Can't check for IsWinEventHookInstalled(EVENT_CONSOLE_CARET), // because we need to emit a ConsoleControl() call regardless. - if (_msaaEnabled) + if (_msaaEnabled || uiaEnabled) { const auto guard = _lock.lock_exclusive(); - _state.eventConsoleCaretPositionX = position.x; - _state.eventConsoleCaretPositionY = position.y; - _state.eventConsoleCaretSelecting = activeSelection; - _state.eventConsoleCaretPrimed = true; + if (_msaaEnabled) + { + _state.eventConsoleCaretPositionX = position.x; + _state.eventConsoleCaretPositionY = position.y; + _state.eventConsoleCaretSelecting = activeSelection; + _state.eventConsoleCaretPrimed = true; + } + + if (uiaEnabled) + { + _state.textSelectionChanged = true; + } _timerSet(); } @@ -314,7 +328,7 @@ void AccessibilityNotifier::_timerSet() noexcept { if (!_delay) { - _emitMSAA(_state); + _emitEvents(_state); } else if (!_state.timerScheduled) { @@ -349,40 +363,79 @@ void NTAPI AccessibilityNotifier::_timerEmitMSAA(PTP_CALLBACK_INSTANCE, PVOID co memset(&self->_state, 0, sizeof(State)); } - self->_emitMSAA(state); + self->_emitEvents(state); } -void AccessibilityNotifier::_emitMSAA(State& state) const noexcept +void AccessibilityNotifier::_emitEvents(State& state) const noexcept { const auto cc = ServiceLocator::LocateConsoleControl(); const auto provider = _uiaProvider.load(std::memory_order_relaxed); + LONG updateRegionBeg = 0; + LONG updateRegionEnd = 0; + LONG updateSimpleCharAndAttr = 0; + LONG caretPosition = 0; + std::optional caretInfo; - if (state.eventConsoleCaretPrimed) + // vvv Prepare any information we need vvv + // + // Because NotifyWinEvent and UiaRaiseAutomationEvent are _very_ slow, + // and the following needs the console lock, we do it separately first. + + if (state.eventConsoleUpdateRegionPrimed || state.eventConsoleCaretPrimed) { - const auto x = castSaturated(state.eventConsoleCaretPositionX); - const auto y = castSaturated(state.eventConsoleCaretPositionY); - // Technically, CONSOLE_CARET_SELECTION and CONSOLE_CARET_VISIBLE are bitflags, - // however Microsoft's _own_ example code for these assumes that they're an - // enumeration and also assumes that a value of 0 (= invisible cursor) is invalid. - // So, we just pretend as if the cursor is always visible. - const auto flags = state.eventConsoleCaretSelecting ? CONSOLE_CARET_SELECTION : CONSOLE_CARET_VISIBLE; + auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); + gci.LockConsole(); - // There's no need to check for IsWinEventHookInstalled, - // because NotifyWinEvent is very fast if no event is installed. - cc->NotifyWinEvent(EVENT_CONSOLE_CARET, _hwnd, flags, MAKELONG(x, y)); + if (state.eventConsoleUpdateRegionPrimed) + { + const auto regionBegX = castSaturated(state.eventConsoleUpdateRegionBeginX); + const auto regionBegY = castSaturated(state.eventConsoleUpdateRegionBeginY); + const auto regionEndX = castSaturated(state.eventConsoleUpdateRegionEndX); + const auto regionEndY = castSaturated(state.eventConsoleUpdateRegionEndY); + updateRegionBeg = MAKELONG(regionBegX, regionBegY); + updateRegionEnd = MAKELONG(regionEndX, regionEndY); + + // Historically we'd emit an EVENT_CONSOLE_UPDATE_SIMPLE event for single-char updates, + // but in the 30 years since, the way fast software is written has changed: + // We now have plenty CPU power but the speed of light is still the same. + // It's much more important to batch events to avoid NotifyWinEvent's latency problems. + // EVENT_CONSOLE_UPDATE_SIMPLE is not trivially batch-able, so we should avoid it. + // + // That said, NVDA is currently a very popular screen reader for Windows. + // IF you set its "Windows Console support" to "Legacy" AND disable + // "Use enhanced typed character support in legacy Windows Console when available" + // then it will purely rely on these WinEvents for accessibility. + // + // In this case it assumes that EVENT_CONSOLE_UPDATE_REGION is regular output + // and that EVENT_CONSOLE_UPDATE_SIMPLE is keyboard input (FYI: don't do this). + // The problem now is that it doesn't announce any EVENT_CONSOLE_UPDATE_REGION + // events where beg == end (i.e. a single character change). + // + // Unfortunately, the same is partially true for Microsoft's own Narrator. + if (gci.HasActiveOutputBuffer() && updateRegionBeg == updateRegionEnd) + { + auto& screenInfo = gci.GetActiveOutputBuffer(); + auto& buffer = screenInfo.GetTextBuffer(); + const auto& row = buffer.GetRowByOffset(regionBegY); + const auto glyph = row.GlyphAt(regionBegX); + const auto attr = row.GetAttrByColumn(regionBegX); + updateSimpleCharAndAttr = MAKELONG(Utf16ToUcs2(glyph), attr.GetLegacyAttributes()); + } + } + if (state.eventConsoleCaretPrimed) { - std::optional caretInfo; + const auto caretX = castSaturated(state.eventConsoleCaretPositionX); + const auto caretY = castSaturated(state.eventConsoleCaretPositionY); + caretPosition = MAKELONG(caretX, caretY); // Convert the buffer position to the equivalent screen coordinates // required by CONSOLE_CARET_INFO, taking line rendition into account. - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.LockConsole(); if (gci.HasActiveOutputBuffer()) { auto& screenInfo = gci.GetActiveOutputBuffer(); auto& buffer = screenInfo.GetTextBuffer(); - const auto position = buffer.BufferToScreenPosition({ x, y }); + const auto position = buffer.BufferToScreenPosition({ caretX, caretY }); const auto viewport = screenInfo.GetViewport(); const auto fontSize = screenInfo.GetScreenFontSize(); const auto left = (position.x - viewport.Left()) * fontSize.width; @@ -397,80 +450,34 @@ void AccessibilityNotifier::_emitMSAA(State& state) const noexcept }, }); } - gci.UnlockConsole(); - - if (caretInfo) - { - cc->Control(ControlType::ConsoleSetCaretInfo, &*caretInfo, sizeof(*caretInfo)); - } } - state.eventConsoleCaretPositionX = 0; - state.eventConsoleCaretPositionY = 0; - state.eventConsoleCaretSelecting = false; - state.eventConsoleCaretPrimed = false; + gci.UnlockConsole(); } + // vvv Raise events now vvv + // + // NOTE: When typing in a cooked read prompt (e.g. cmd.exe), the following events + // are historically raised synchronously/immediately in the listed order: + // * NotifyWinEvent(EVENT_CONSOLE_UPDATE_SIMPLE) + // * UiaRaiseAutomationEvent(UIA_Text_TextChangedEventId) + // + // Then, between 0-530ms later, via the now removed blink timer routine, + // the following was raised asynchronously: + // * ConsoleControl(ConsoleSetCaretInfo) + // * NotifyWinEvent(EVENT_CONSOLE_CARET) + // * UiaRaiseAutomationEvent(UIA_Text_TextSelectionChangedEventId) + if (state.eventConsoleUpdateRegionPrimed) { - const auto begX = castSaturated(state.eventConsoleUpdateRegionBeginX); - const auto begY = castSaturated(state.eventConsoleUpdateRegionBeginY); - const auto endX = castSaturated(state.eventConsoleUpdateRegionEndX); - const auto endY = castSaturated(state.eventConsoleUpdateRegionEndY); - const auto beg = MAKELONG(begX, begY); - const auto end = MAKELONG(endX, endY); - - // Previously, we'd also emit a EVENT_CONSOLE_UPDATE_SIMPLE event for single-char updates, - // but in the 30 years since, the way fast software is written has changed: - // We now have plenty CPU power but the speed of light is still the same. - // It's much more important to batch events to avoid NotifyWinEvent's latency problems. - // EVENT_CONSOLE_UPDATE_SIMPLE is not trivially batch-able and so it got removed. - // - // That said, NVDA is currently a very popular screen reader for Windows. - // IF you set its "Windows Console support" to "Legacy" AND disable - // "Use enhanced typed character support in legacy Windows Console when available" - // then it will purely rely on these WinEvents for accessibility. - // - // In this case it assumes that EVENT_CONSOLE_UPDATE_REGION is regular output - // and that EVENT_CONSOLE_UPDATE_SIMPLE is keyboard input (FYI: don't do this). - // The problem now is that it doesn't announce any EVENT_CONSOLE_UPDATE_REGION - // events where beg == end (i.e. a single character change). - // - // The good news is that if you set these two options in NVDA, it crashes whenever - // any conhost instance exits, so... maybe we don't need to work around this? :D - // - // I'll leave this code here, in case we ever need to shim EVENT_CONSOLE_UPDATE_SIMPLE. -#if 0 - LONG charAndAttr = 0; - - if (beg == end) - { - auto& gci = ServiceLocator::LocateGlobals().getConsoleInformation(); - gci.LockConsole(); - - if (gci.HasActiveOutputBuffer()) - { - auto& tb = gci.GetActiveOutputBuffer().GetTextBuffer(); - const auto& row = tb.GetRowByOffset(begY); - const auto glyph = row.GlyphAt(begX); - const auto attr = row.GetAttrByColumn(begX); - charAndAttr = MAKELONG(Utf16ToUcs2(glyph), attr.GetLegacyAttributes()); - } - - gci.UnlockConsole(); - } - - if (charAndAttr) + if (updateSimpleCharAndAttr) { - cc->NotifyWinEvent(EVENT_CONSOLE_UPDATE_SIMPLE, _hwnd, beg, charAndAttr); + cc->NotifyWinEvent(EVENT_CONSOLE_UPDATE_SIMPLE, _hwnd, updateRegionBeg, updateSimpleCharAndAttr); } else { - cc->NotifyWinEvent(EVENT_CONSOLE_UPDATE_REGION, _hwnd, beg, end); + cc->NotifyWinEvent(EVENT_CONSOLE_UPDATE_REGION, _hwnd, updateRegionBeg, updateRegionEnd); } -#else - cc->NotifyWinEvent(EVENT_CONSOLE_UPDATE_REGION, _hwnd, beg, end); -#endif state.eventConsoleUpdateRegionBeginX = 0; state.eventConsoleUpdateRegionBeginY = 0; @@ -479,6 +486,41 @@ void AccessibilityNotifier::_emitMSAA(State& state) const noexcept state.eventConsoleUpdateRegionPrimed = false; } + if (state.textChanged) + { + _emitUIAEvent(provider, UIA_Text_TextChangedEventId); + state.textChanged = false; + } + + if (state.eventConsoleCaretPrimed) + { + if (caretInfo) + { + cc->Control(ControlType::ConsoleSetCaretInfo, &*caretInfo, sizeof(*caretInfo)); + } + + // There's no need to check for IsWinEventHookInstalled, + // because NotifyWinEvent is very fast if no event is installed. + // + // Technically, CONSOLE_CARET_SELECTION and CONSOLE_CARET_VISIBLE are bitflags, + // however Microsoft's _own_ example code for these assumes that they're an + // enumeration and also assumes that a value of 0 (= invisible cursor) is invalid. + // So, we just pretend as if the cursor is always visible. + const auto flags = state.eventConsoleCaretSelecting ? CONSOLE_CARET_SELECTION : CONSOLE_CARET_VISIBLE; + cc->NotifyWinEvent(EVENT_CONSOLE_CARET, _hwnd, flags, caretPosition); + + state.eventConsoleCaretPositionX = 0; + state.eventConsoleCaretPositionY = 0; + state.eventConsoleCaretSelecting = false; + state.eventConsoleCaretPrimed = false; + } + + if (state.textSelectionChanged) + { + _emitUIAEvent(provider, UIA_Text_TextSelectionChangedEventId); + state.textSelectionChanged = false; + } + if (state.eventConsoleUpdateScrollPrimed) { const auto dx = castSaturated(state.eventConsoleUpdateScrollDeltaX); @@ -496,18 +538,6 @@ void AccessibilityNotifier::_emitMSAA(State& state) const noexcept cc->NotifyWinEvent(EVENT_CONSOLE_LAYOUT, _hwnd, 0, 0); state.eventConsoleLayoutPrimed = false; } - - if (state.textSelectionChanged) - { - _emitUIAEvent(provider, UIA_Text_TextSelectionChangedEventId); - state.textSelectionChanged = false; - } - - if (state.textChanged) - { - _emitUIAEvent(provider, UIA_Text_TextChangedEventId); - state.textChanged = false; - } } void AccessibilityNotifier::_emitUIAEvent(IRawElementProviderSimple* provider, EVENTID id) noexcept diff --git a/src/host/AccessibilityNotifier.h b/src/host/AccessibilityNotifier.h index 332a5994625..8484562eb01 100644 --- a/src/host/AccessibilityNotifier.h +++ b/src/host/AccessibilityNotifier.h @@ -71,7 +71,7 @@ namespace Microsoft::Console PTP_TIMER _createTimer(PTP_TIMER_CALLBACK callback) noexcept; void _timerSet() noexcept; static void NTAPI _timerEmitMSAA(PTP_CALLBACK_INSTANCE instance, PVOID context, PTP_TIMER timer) noexcept; - void _emitMSAA(State& msaa) const noexcept; + void _emitEvents(State& msaa) const noexcept; static void _emitUIAEvent(IRawElementProviderSimple* provider, EVENTID id) noexcept; // The main window, used for NotifyWinEvent / ConsoleControl(ConsoleSetCaretInfo) calls. diff --git a/src/host/selection.cpp b/src/host/selection.cpp index 174782a056e..40e675d6b80 100644 --- a/src/host/selection.cpp +++ b/src/host/selection.cpp @@ -177,9 +177,6 @@ void Selection::InitializeMouseSelection(const til::point coordBufferPos) if (pWindow != nullptr) { pWindow->UpdateWindowText(); - - auto& an = ServiceLocator::LocateGlobals().accessibilityNotifier; - an.SelectionChanged(); } // Fire off an event to let accessibility apps know the selection has changed. @@ -302,7 +299,6 @@ void Selection::_ExtendSelection(Selection::SelectionData* d, _In_ til::point co // Fire off an event to let accessibility apps know the selection has changed. auto& an = ServiceLocator::LocateGlobals().accessibilityNotifier; an.CursorChanged(coordBufferPos, true); - an.SelectionChanged(); } // Routine Description: From 55e3efebcdfec0e308e7bb5c0c6b75e5dcaf8b6a Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Wed, 15 Oct 2025 16:10:23 +0200 Subject: [PATCH 6/8] Fix build --- src/buffer/out/cursor.cpp | 4 ++-- src/buffer/out/cursor.h | 4 ++-- src/renderer/base/renderer.hpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/buffer/out/cursor.cpp b/src/buffer/out/cursor.cpp index 04548d5505b..70a0956631e 100644 --- a/src/buffer/out/cursor.cpp +++ b/src/buffer/out/cursor.cpp @@ -47,7 +47,7 @@ ULONG Cursor::GetSize() const noexcept return _ulSize; } -void Cursor::SetIsVisible(bool enable) +void Cursor::SetIsVisible(bool enable) noexcept { if (_isVisible != enable) { @@ -56,7 +56,7 @@ void Cursor::SetIsVisible(bool enable) } } -void Cursor::SetIsBlinking(bool enable) +void Cursor::SetIsBlinking(bool enable) noexcept { if (_isBlinking != enable) { diff --git a/src/buffer/out/cursor.h b/src/buffer/out/cursor.h index ed952c23e45..4405278e199 100644 --- a/src/buffer/out/cursor.h +++ b/src/buffer/out/cursor.h @@ -44,8 +44,8 @@ class Cursor final til::point GetPosition() const noexcept; CursorType GetType() const noexcept; - void SetIsVisible(bool enable); - void SetIsBlinking(bool enable); + void SetIsVisible(bool enable) noexcept; + void SetIsBlinking(bool enable) noexcept; void SetIsDouble(const bool fIsDouble) noexcept; void SetSize(const ULONG ulSize) noexcept; void SetStyle(const ULONG ulSize, const CursorType type) noexcept; diff --git a/src/renderer/base/renderer.hpp b/src/renderer/base/renderer.hpp index 1cd157957ca..25694e6845e 100644 --- a/src/renderer/base/renderer.hpp +++ b/src/renderer/base/renderer.hpp @@ -97,7 +97,7 @@ namespace Microsoft::Console::Render static bool s_IsSoftFontChar(const std::wstring_view& v, const size_t firstSoftFontChar, const size_t lastSoftFontChar); // Base rendering loop - static DWORD s_renderThread(void*) noexcept; + static DWORD WINAPI s_renderThread(void*) noexcept; DWORD _renderThread() noexcept; void _waitUntilCanRender() noexcept; From dfad2d956b07293d10fa02a4ae43259c0d7eda79 Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 Oct 2025 15:05:42 +0200 Subject: [PATCH 7/8] Address feedback, Fix broadcast input --- src/cascadia/TerminalControl/ControlCore.cpp | 14 ++- src/cascadia/TerminalControl/ControlCore.h | 3 + src/cascadia/TerminalControl/ControlCore.idl | 1 + src/cascadia/TerminalControl/TermControl.cpp | 15 ++- src/cascadia/TerminalControl/TermControl.h | 1 - src/cascadia/TerminalCore/Terminal.cpp | 5 + src/cascadia/TerminalCore/Terminal.hpp | 1 + .../Profiles_Appearance.cpp | 1 - src/host/AccessibilityNotifier.cpp | 8 +- src/renderer/atlas/README.md | 3 - src/renderer/base/renderer.cpp | 2 +- src/renderer/base/sources.inc | 1 - src/renderer/base/thread.cpp | 106 ------------------ src/renderer/base/thread.hpp | 43 ------- src/terminal/adapter/adaptDispatch.cpp | 4 +- 15 files changed, 38 insertions(+), 170 deletions(-) delete mode 100644 src/renderer/base/thread.cpp delete mode 100644 src/renderer/base/thread.hpp diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 82a22c551bf..cffc4e91a8a 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -1969,6 +1969,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation return _terminal->GetViewportRelativeCursorPosition().to_core_point(); } + bool ControlCore::ForceCursorVisible() const noexcept + { + return _forceCursorVisible; + } + + void ControlCore::ForceCursorVisible(bool force) + { + const auto lock = _terminal->LockForWriting(); + _renderer->AllowCursorVisibility(Render::InhibitionSource::Host, _terminal->IsFocused() || force); + _forceCursorVisible = force; + } + // This one's really pushing the boundary of what counts as "encapsulation". // It really belongs in the "Interactivity" layer, which doesn't yet exist. // There's so many accesses to the selection in the Core though, that I just @@ -2437,7 +2449,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TerminalInput::OutputType out; { const auto lock = _terminal->LockForWriting(); - _renderer->AllowCursorVisibility(Render::InhibitionSource::Host, focused); + _renderer->AllowCursorVisibility(Render::InhibitionSource::Host, focused || _forceCursorVisible); out = _terminal->FocusChanged(focused); } if (out && !out->empty()) diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 17db2145a9d..0f911d61e97 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -217,6 +217,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool IsVtMouseModeEnabled() const; bool ShouldSendAlternateScroll(const unsigned int uiButton, const int32_t delta) const; Core::Point CursorPosition() const; + bool ForceCursorVisible() const noexcept; + void ForceCursorVisible(bool force); bool CopyOnSelect() const; Control::SelectionData SelectionInfo() const; @@ -396,6 +398,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation float _panelWidth{ 0 }; float _panelHeight{ 0 }; float _compositionScale{ 0 }; + bool _forceCursorVisible = false; // Audio stuff. MidiAudio _midiAudio; diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 62488c3aaea..d9f92e011b8 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -150,6 +150,7 @@ namespace Microsoft.Terminal.Control void SetReadOnlyMode(Boolean readOnlyState); Microsoft.Terminal.Core.Point CursorPosition { get; }; + Boolean ForceCursorVisible; void ResumeRendering(); SearchResults Search(SearchRequest request); diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 83743fd3f49..b079dbbc39c 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -4063,18 +4063,21 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.ContextMenuSelectOutput(); } - // Should the text cursor be displayed, even when the control isn't focused? - // n.b. "blur" is the opposite of "focus". - bool TermControl::_displayCursorWhileBlurred() const noexcept - { - return CursorVisibility() == Control::CursorDisplayState::Shown; - } Control::CursorDisplayState TermControl::CursorVisibility() const noexcept { return _cursorVisibility; } + void TermControl::CursorVisibility(Control::CursorDisplayState cursorVisibility) { _cursorVisibility = cursorVisibility; + + // NOTE: This code is specific to broadcast input. It's never been well integrated. + // Ideally TermControl should not tie focus to XAML in the first place, + // allowing us to truly say "yeah these two controls both have focus". + if (_core) + { + _core.ForceCursorVisible(cursorVisibility == CursorDisplayState::Shown); + } } } diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index d9f15b9a3ac..65276a00565 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -445,7 +445,6 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _SelectCommandHandler(const IInspectable& sender, const IInspectable& args); void _SelectOutputHandler(const IInspectable& sender, const IInspectable& args); - bool _displayCursorWhileBlurred() const noexcept; struct Revokers { diff --git a/src/cascadia/TerminalCore/Terminal.cpp b/src/cascadia/TerminalCore/Terminal.cpp index 563e54bb992..9c3a9b25b74 100644 --- a/src/cascadia/TerminalCore/Terminal.cpp +++ b/src/cascadia/TerminalCore/Terminal.cpp @@ -1034,6 +1034,11 @@ int Terminal::ViewEndIndex() const noexcept return _inAltBuffer() ? _altBufferSize.height - 1 : _mutableViewport.BottomInclusive(); } +bool Terminal::IsFocused() const noexcept +{ + return _focused; +} + RenderSettings& Terminal::GetRenderSettings() noexcept { _assertLocked(); diff --git a/src/cascadia/TerminalCore/Terminal.hpp b/src/cascadia/TerminalCore/Terminal.hpp index 7c27df47a56..5bd67f87e72 100644 --- a/src/cascadia/TerminalCore/Terminal.hpp +++ b/src/cascadia/TerminalCore/Terminal.hpp @@ -114,6 +114,7 @@ class Microsoft::Terminal::Core::Terminal final : int ViewStartIndex() const noexcept; int ViewEndIndex() const noexcept; + bool IsFocused() const noexcept; RenderSettings& GetRenderSettings() noexcept; const RenderSettings& GetRenderSettings() const noexcept; diff --git a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp index c338925ee3a..9884ba8e759 100644 --- a/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp +++ b/src/cascadia/TerminalSettingsEditor/Profiles_Appearance.cpp @@ -33,7 +33,6 @@ namespace winrt::Microsoft::Terminal::Settings::Editor::implementation _previewControl = Control::TermControl(settings, settings, *_previewConnection); _previewControl.IsEnabled(false); _previewControl.AllowFocusWhenDisabled(false); - _previewControl.CursorVisibility(Microsoft::Terminal::Control::CursorDisplayState::Shown); ControlPreview().Child(_previewControl); } diff --git a/src/host/AccessibilityNotifier.cpp b/src/host/AccessibilityNotifier.cpp index a2272240f7e..8062c42e7d0 100644 --- a/src/host/AccessibilityNotifier.cpp +++ b/src/host/AccessibilityNotifier.cpp @@ -71,6 +71,10 @@ void AccessibilityNotifier::Initialize(HWND hwnd, DWORD msaaDelay, DWORD uiaDela void AccessibilityNotifier::SetUIAProvider(IRawElementProviderSimple* provider) noexcept { + // NOTE: The assumption is that you're holding the console lock when calling any of the member functions. + // This is why we can safely update these members (no worker thread is running nor can be scheduled). + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); + // If UIA events are disabled, don't set _uiaProvider either. // It would trigger unnecessary work. if (!_uiaEnabled) @@ -78,10 +82,6 @@ void AccessibilityNotifier::SetUIAProvider(IRawElementProviderSimple* provider) return; } - // NOTE: The assumption is that you're holding the console lock when calling any of the member functions. - // This is why we can safely update these members (no worker thread is running nor can be scheduled). - assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); - // Of course we must ensure our precious provider object doesn't go away. if (provider) { diff --git a/src/renderer/atlas/README.md b/src/renderer/atlas/README.md index a1619e9b4bc..f15f1195b63 100644 --- a/src/renderer/atlas/README.md +++ b/src/renderer/atlas/README.md @@ -4,7 +4,6 @@ ```mermaid graph TD - RenderThread["RenderThread (base/thread.cpp)
calls Renderer::PaintFrame() x times per sec"] Renderer["Renderer (base/renderer.cpp)
breaks the text buffer down into GDI-oriented graphics
primitives (#quot;change brush to color X#quot;, #quot;draw string Y#quot;, ...)
"] RenderEngineBase[/"RenderEngineBase
(base/RenderEngineBase.cpp)
abstracts 24 LOC 👻"\] GdiEngine["GdiEngine (gdi/...)"] @@ -18,8 +17,6 @@ graph TD BackendD3D.cpp["BackendD3D.cpp
Custom, performant text renderer
with our own glyph cache
"] end - RenderThread --> Renderer - Renderer -->|owns| RenderThread Renderer -.-> RenderEngineBase %% Mermaid.js has no support for backwards arrow at the moment RenderEngineBase <-.->|extends| GdiEngine diff --git a/src/renderer/base/renderer.cpp b/src/renderer/base/renderer.cpp index 15bf1b3b303..a89efed9157 100644 --- a/src/renderer/base/renderer.cpp +++ b/src/renderer/base/renderer.cpp @@ -1493,7 +1493,7 @@ void Renderer::_updateCursorInfo() else if (!IsTimerRunning(_cursorBlinker)) { const auto actual = GetTimerInterval(_cursorBlinker); - auto expected = _pData->GetBlinkInterval(); + const auto expected = _pData->GetBlinkInterval(); if (expected > TimerDuration::zero() && expected < TimerDuration::max()) { diff --git a/src/renderer/base/sources.inc b/src/renderer/base/sources.inc index 7a3781a419a..e62da9a8352 100644 --- a/src/renderer/base/sources.inc +++ b/src/renderer/base/sources.inc @@ -32,7 +32,6 @@ SOURCES = \ ..\RenderEngineBase.cpp \ ..\RenderSettings.cpp \ ..\renderer.cpp \ - ..\thread.cpp \ INCLUDES = \ $(INCLUDES); \ diff --git a/src/renderer/base/thread.cpp b/src/renderer/base/thread.cpp deleted file mode 100644 index 39b7ec60783..00000000000 --- a/src/renderer/base/thread.cpp +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -#include "precomp.h" -#include "thread.hpp" - -#include "renderer.hpp" - -#pragma hdrstop - -using namespace Microsoft::Console::Render; - -RenderThread::RenderThread(Renderer* renderer) : - renderer(renderer) -{ -} - -RenderThread::~RenderThread() -{ - TriggerTeardown(); -} - -DWORD WINAPI RenderThread::s_ThreadProc(_In_ LPVOID lpParameter) -{ - const auto pContext = static_cast(lpParameter); - return pContext->_ThreadProc(); -} - -DWORD WINAPI RenderThread::_ThreadProc() -{ - while (true) - { - _enable.wait(); - - // Between waiting on _hEvent and calling PaintFrame() there should be a minimal delay, - // so that a key press progresses to a drawing operation as quickly as possible. - // As such, we wait for the renderer to complete _before_ waiting on `_redraw`. - renderer->WaitUntilCanRender(); - - _redraw.wait(); - if (!_keepRunning.load(std::memory_order_relaxed)) - { - break; - } - - LOG_IF_FAILED(renderer->PaintFrame()); - } - - return S_OK; -} - -void RenderThread::NotifyPaint() noexcept -{ - _redraw.SetEvent(); -} - -// Spawns a new rendering thread if none exists yet. -void RenderThread::EnablePainting() noexcept -{ - const auto guard = _threadMutex.lock_exclusive(); - - _enable.SetEvent(); - - if (!_thread) - { - _keepRunning.store(true, std::memory_order_relaxed); - - _thread.reset(CreateThread(nullptr, 0, s_ThreadProc, this, 0, nullptr)); - THROW_LAST_ERROR_IF(!_thread); - - // SetThreadDescription only works on 1607 and higher. If we cannot find it, - // then it's no big deal. Just skip setting the description. - const auto func = GetProcAddressByFunctionDeclaration(GetModuleHandleW(L"kernel32.dll"), SetThreadDescription); - if (func) - { - LOG_IF_FAILED(func(_thread.get(), L"Rendering Output Thread")); - } - } -} - -// This function is meant to only be called by `Renderer`. You should use `TriggerTeardown()` instead, -// even if you plan to call `EnablePainting()` later, because that ensures proper synchronization. -void RenderThread::DisablePainting() noexcept -{ - _enable.ResetEvent(); -} - -// Stops the rendering thread, and waits for it to finish. -void RenderThread::TriggerTeardown() noexcept -{ - const auto guard = _threadMutex.lock_exclusive(); - - if (_thread) - { - // The render thread first waits for the event and then checks _keepRunning. By doing it - // in reverse order here, we ensure that it's impossible for the render thread to miss this. - _keepRunning.store(false, std::memory_order_relaxed); - _redraw.SetEvent(); - _enable.SetEvent(); - - WaitForSingleObject(_thread.get(), INFINITE); - _thread.reset(); - } - - DisablePainting(); -} diff --git a/src/renderer/base/thread.hpp b/src/renderer/base/thread.hpp deleted file mode 100644 index fbc921465db..00000000000 --- a/src/renderer/base/thread.hpp +++ /dev/null @@ -1,43 +0,0 @@ -/*++ -Copyright (c) Microsoft Corporation -Licensed under the MIT license. - -Module Name: -- Thread.hpp - -Abstract: -- This is the definition of our rendering thread designed to throttle and compartmentalize drawing operations. - -Author(s): -- Michael Niksa (MiNiksa) Feb 2016 ---*/ - -#pragma once - -namespace Microsoft::Console::Render -{ - class Renderer; - - class RenderThread - { - public: - RenderThread(Renderer* renderer); - ~RenderThread(); - - void NotifyPaint() noexcept; - void EnablePainting() noexcept; - void DisablePainting() noexcept; - void TriggerTeardown() noexcept; - - private: - static DWORD WINAPI s_ThreadProc(_In_ LPVOID lpParameter); - DWORD WINAPI _ThreadProc(); - - Renderer* renderer; - wil::slim_event_manual_reset _enable; - wil::slim_event_auto_reset _redraw; - wil::srwlock _threadMutex; - wil::unique_handle _thread; - std::atomic _keepRunning{ false }; - }; -} diff --git a/src/terminal/adapter/adaptDispatch.cpp b/src/terminal/adapter/adaptDispatch.cpp index 7c1075a5943..9de1509bd73 100644 --- a/src/terminal/adapter/adaptDispatch.cpp +++ b/src/terminal/adapter/adaptDispatch.cpp @@ -2434,9 +2434,7 @@ bool AdaptDispatch::_DoLineFeed(const Page& page, const bool withReturn, const b textBuffer.IncrementCircularBuffer(eraseAttributes); _api.NotifyBufferRotation(1); - // We trigger a scroll rather than a redraw, since that's more efficient, - // but we need to turn the cursor off before doing so; otherwise, a ghost - // cursor can be left behind in the previous position. + // We trigger a scroll rather than a redraw, since that's more efficient. textBuffer.TriggerScroll({ 0, -1 }); // And again, if the bottom margin didn't cover the full page, we From 99c61934cdc565015907e4ce474567d24103be5a Mon Sep 17 00:00:00 2001 From: Leonard Hecker Date: Thu, 16 Oct 2025 18:52:04 +0200 Subject: [PATCH 8/8] Fix assert --- src/host/AccessibilityNotifier.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/host/AccessibilityNotifier.cpp b/src/host/AccessibilityNotifier.cpp index 8062c42e7d0..c46038d5047 100644 --- a/src/host/AccessibilityNotifier.cpp +++ b/src/host/AccessibilityNotifier.cpp @@ -71,17 +71,19 @@ void AccessibilityNotifier::Initialize(HWND hwnd, DWORD msaaDelay, DWORD uiaDela void AccessibilityNotifier::SetUIAProvider(IRawElementProviderSimple* provider) noexcept { - // NOTE: The assumption is that you're holding the console lock when calling any of the member functions. - // This is why we can safely update these members (no worker thread is running nor can be scheduled). - assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); - // If UIA events are disabled, don't set _uiaProvider either. // It would trigger unnecessary work. + // + // NOTE: We check this before the assert() below so that unit tests don't trigger the assert. if (!_uiaEnabled) { return; } + // NOTE: The assumption is that you're holding the console lock when calling any of the member functions. + // This is why we can safely update these members (no worker thread is running nor can be scheduled). + assert(ServiceLocator::LocateGlobals().getConsoleInformation().IsConsoleLocked()); + // Of course we must ensure our precious provider object doesn't go away. if (provider) {