Skip to content

Commit

Permalink
Add support for the "blink" graphic rendition attribute (#7490)
Browse files Browse the repository at this point in the history
This PR adds support for the _blink_ graphic rendition attribute. When a
character is output with this attribute set, it "blinks" at a regular
interval, by cycling its color between the normal rendition and a dimmer
shade of that color.

The majority of the blinking mechanism is encapsulated in a new
`BlinkingState` class, which is shared between the Terminal and Conhost
implementations. This class keeps track of the position in the blinking
cycle, which determines whether characters are rendered as normal or
faint.

In Windows Terminal, the state is stored in the `Terminal` class, and in
Conhost it's stored in the `CONSOLE_INFORMATION` class. In both cases,
the `IsBlinkingFaint` method is used to determine the current blinking
rendition, and that is passed on as a parameter to the
`TextAttribute::CalculateRgbColors` method when these classes are
looking up attribute colors.

Prior to calculating the colors, the current attribute is also passed to
the `RecordBlinkingUsage` method, which keeps track of whether there are
actually any blink attributes in use. This is used to determine whether
the screen needs to be refreshed when the blinking cycle toggles between
the normal and faint renditions.

The refresh itself is handled by the `ToggleBlinkingRendition` method,
which is triggered by a timer. In Conhost this is just piggybacking on
the existing cursor blink timer, but in Windows Terminal it needs to
have its own separate timer, since the cursor timer is reset whenever a
key is pressed, which is not something we want for attribute blinking.

Although the `ToggleBlinkingRendition` is called at the same rate as the
cursor blinking, we actually only want the cells to blink at half that
frequency. We thus have a counter that cycles through four phases, and
blinking is rendered as faint for two of those four. Then every two
cycles - when the state changes - a redraw is triggered, but only if
there are actually blinking attributes in use (as previously recorded).

As mentioned earlier, the blinking frequency is based on the cursor
blink rate, so that means it'll automatically be disabled if a user has
set their cursor blink rate to none. It can also be disabled by turning
off the _Show animations in Windows_ option. In Conhost these settings
take effect immediately, but in Windows Terminal they only apply when a
new tab is opened.

This PR also adds partial support for the `SGR 6` _rapid blink_
attribute. This is not used by DEC terminals, but was defined in the
ECMA/ANSI standards. It's not widely supported, but many terminals just
it implement it as an alias for the regular `SGR 5` blink attribute, so
that's what I've done here too.

## Validation Steps Performed

I've checked the _Graphic rendition test pattern_ in Vttest, and
compared our representation of the blink attribute to that of an actual
DEC VT220 terminal as seen on [YouTube]. With the right color scheme
it's a reasonably close match.

[YouTube]: https://www.youtube.com/watch?v=03Pz5AmxbE4&t=1m55s

Closes #7388

(cherry picked from commit d1671a0)
  • Loading branch information
j4james authored and DHowett committed Sep 21, 2020
1 parent 21ab0a8 commit 825039e
Show file tree
Hide file tree
Showing 20 changed files with 245 additions and 59 deletions.
1 change: 1 addition & 0 deletions .github/actions/spell-check/expect/expect.txt
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,7 @@ GETAUTOHIDEBAREX
GETCARETWIDTH
getch
getchar
GETCLIENTAREAANIMATION
GETCOMMANDHISTORY
GETCOMMANDHISTORYLENGTH
GETCONSOLEINPUT
Expand Down
6 changes: 4 additions & 2 deletions src/buffer/out/TextAttribute.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,18 @@ bool TextAttribute::IsLegacy() const noexcept
// - defaultFgColor: the default foreground color rgb value.
// - defaultBgColor: the default background color rgb value.
// - reverseScreenMode: true if the screen mode is reversed.
// - blinkingIsFaint: true if blinking should be interpreted as faint.
// Return Value:
// - the foreground and background colors that should be displayed.
std::pair<COLORREF, COLORREF> TextAttribute::CalculateRgbColors(const gsl::span<const COLORREF> colorTable,
const COLORREF defaultFgColor,
const COLORREF defaultBgColor,
const bool reverseScreenMode) const noexcept
const bool reverseScreenMode,
const bool blinkingIsFaint) const noexcept
{
auto fg = _foreground.GetColor(colorTable, defaultFgColor, IsBold());
auto bg = _background.GetColor(colorTable, defaultBgColor);
if (IsFaint())
if (IsFaint() || (IsBlinking() && blinkingIsFaint))
{
fg = (fg >> 1) & 0x7F7F7F; // Divide foreground color components by two.
}
Expand Down
3 changes: 2 additions & 1 deletion src/buffer/out/TextAttribute.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ class TextAttribute final
std::pair<COLORREF, COLORREF> CalculateRgbColors(const gsl::span<const COLORREF> colorTable,
const COLORREF defaultFgColor,
const COLORREF defaultBgColor,
const bool reverseScreenMode = false) const noexcept;
const bool reverseScreenMode = false,
const bool blinkingIsFaint = false) const noexcept;

bool IsLeadingByte() const noexcept;
bool IsTrailingByte() const noexcept;
Expand Down
45 changes: 45 additions & 0 deletions src/cascadia/TerminalControl/TermControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_actualFont{ DEFAULT_FONT_FACE, 0, DEFAULT_FONT_WEIGHT, { 0, DEFAULT_FONT_SIZE }, CP_UTF8, false },
_touchAnchor{ std::nullopt },
_cursorTimer{},
_blinkTimer{},
_lastMouseClickTimestamp{},
_lastMouseClickPos{},
_selectionNeedsToBeCopied{ false },
Expand Down Expand Up @@ -721,6 +722,24 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_cursorTimer = std::nullopt;
}

// Set up blinking attributes
BOOL animationsEnabled = TRUE;
SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0);
if (animationsEnabled && blinkTime != INFINITE)
{
// Create a timer
DispatcherTimer blinkTimer;
blinkTimer.Interval(std::chrono::milliseconds(blinkTime));
blinkTimer.Tick({ get_weak(), &TermControl::_BlinkTimerTick });
blinkTimer.Start();
_blinkTimer.emplace(std::move(blinkTimer));
}
else
{
// The user has disabled blinking
_blinkTimer = std::nullopt;
}

// import value from WinUser (convert from milli-seconds to micro-seconds)
_multiClickTimer = GetDoubleClickTime() * 1000;

Expand Down Expand Up @@ -1777,6 +1796,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_cursorTimer.value().Start();
}

if (_blinkTimer.has_value())
{
_blinkTimer.value().Start();
}

_UpdateSystemParameterSettings();
}

Expand Down Expand Up @@ -1822,6 +1846,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_cursorTimer.value().Stop();
_terminal->SetCursorOn(false);
}

if (_blinkTimer.has_value())
{
_blinkTimer.value().Stop();
}
}

// Method Description:
Expand Down Expand Up @@ -2035,6 +2064,22 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_terminal->SetCursorOn(!_terminal->IsCursorOn());
}

// Method Description:
// - Toggle the blinking rendition state when called by the blink timer.
// Arguments:
// - sender: not used
// - e: not used
void TermControl::_BlinkTimerTick(Windows::Foundation::IInspectable const& /* sender */,
Windows::Foundation::IInspectable const& /* e */)
{
if (!_closing)
{
auto& renderTarget = *_renderer;
auto& blinkingState = _terminal->GetBlinkingState();
blinkingState.ToggleBlinkingRendition(renderTarget);
}
}

// Method Description:
// - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging.
// Arguments:
Expand Down
2 changes: 2 additions & 0 deletions src/cascadia/TerminalControl/TermControl.h
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
std::optional<wchar_t> _leadingSurrogate;

std::optional<Windows::UI::Xaml::DispatcherTimer> _cursorTimer;
std::optional<Windows::UI::Xaml::DispatcherTimer> _blinkTimer;

// If this is set, then we assume we are in the middle of panning the
// viewport via touch input.
Expand Down Expand Up @@ -251,6 +252,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void _HyperlinkHandler(const std::wstring_view uri);

void _CursorTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);
void _BlinkTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);
void _SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition);
void _SendInputToConnection(const winrt::hstring& wstr);
void _SendInputToConnection(std::wstring_view wstr);
Expand Down
5 changes: 5 additions & 0 deletions src/cascadia/TerminalCore/Terminal.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1036,3 +1036,8 @@ const std::optional<til::color> Terminal::GetTabColor() const noexcept
{
return _tabColor;
}

BlinkingState& Terminal::GetBlinkingState() const noexcept
{
return _blinkingState;
}
5 changes: 4 additions & 1 deletion src/cascadia/TerminalCore/Terminal.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#include <conattrs.hpp>

#include "../../buffer/out/textBuffer.hpp"
#include "../../renderer/inc/IRenderData.hpp"
#include "../../renderer/inc/BlinkingState.hpp"
#include "../../terminal/parser/StateMachine.hpp"
#include "../../terminal/input/terminalInput.hpp"

Expand Down Expand Up @@ -187,6 +187,8 @@ class Microsoft::Terminal::Core::Terminal final :

const std::optional<til::color> GetTabColor() const noexcept;

Microsoft::Console::Render::BlinkingState& GetBlinkingState() const noexcept;

#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp
enum class SelectionExpansionMode
Expand Down Expand Up @@ -224,6 +226,7 @@ class Microsoft::Terminal::Core::Terminal final :
COLORREF _defaultBg;
CursorType _defaultCursorShape;
bool _screenReversed;
mutable Microsoft::Console::Render::BlinkingState _blinkingState;

bool _snapOnInput;
bool _altGrAliasing;
Expand Down
1 change: 1 addition & 0 deletions src/cascadia/TerminalCore/TerminalDispatchGraphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ bool TerminalDispatch::SetGraphicsRendition(const gsl::span<const DispatchTypes:
attr.SetItalic(false);
break;
case BlinkOrXterm256Index:
case RapidBlink: // We just interpret rapid blink as an alias of blink.
attr.SetBlinking(true);
break;
case Steady:
Expand Down
4 changes: 3 additions & 1 deletion src/cascadia/TerminalCore/terminalrenderdata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ const TextAttribute Terminal::GetDefaultBrushColors() noexcept

std::pair<COLORREF, COLORREF> Terminal::GetAttributeColors(const TextAttribute& attr) const noexcept
{
_blinkingState.RecordBlinkingUsage(attr);
auto colors = attr.CalculateRgbColors({ _colorTable.data(), _colorTable.size() },
_defaultFg,
_defaultBg,
_screenReversed);
_screenReversed,
_blinkingState.IsBlinkingFaint());
colors.first |= 0xff000000;
// We only care about alpha for the default BG (which enables acrylic)
// If the bg isn't the default bg color, or reverse video is enabled, make it fully opaque.
Expand Down
16 changes: 13 additions & 3 deletions src/host/CursorBlinker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ void CursorBlinker::UpdateSystemMetrics()
{
// 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.
BOOL animationsEnabled = TRUE;
SystemParametersInfoW(SPI_GETCLIENTAREAANIMATION, 0, &animationsEnabled, 0);
auto& blinkingState = ServiceLocator::LocateGlobals().getConsoleInformation().GetBlinkingState();
blinkingState.SetBlinkingAllowed(animationsEnabled && _uCaretBlinkTime != INFINITE);
}

void CursorBlinker::SettingsChanged()
Expand All @@ -53,7 +59,8 @@ void CursorBlinker::FocusStart()
}

// Routine Description:
// - This routine is called when the timer in the console with the focus goes off. It blinks the cursor.
// - 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:
Expand Down Expand Up @@ -109,7 +116,7 @@ void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo)
if (cursor.GetDelay())
{
cursor.SetDelay(false);
goto DoScroll;
goto DoBlinkingRenditionAndScroll;
}

// Don't blink the cursor for remote sessions.
Expand All @@ -118,7 +125,7 @@ void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo)
(!cursor.IsBlinkingAllowed())) &&
cursor.IsOn())
{
goto DoScroll;
goto DoBlinkingRenditionAndScroll;
}

// Blink only if the cursor isn't turned off via the API
Expand All @@ -127,6 +134,9 @@ void CursorBlinker::TimerRoutine(SCREEN_INFORMATION& ScreenInfo)
cursor.SetIsOn(!cursor.IsOn());
}

DoBlinkingRenditionAndScroll:
gci.GetBlinkingState().ToggleBlinkingRendition(ScreenInfo.GetRenderTarget());

DoScroll:
Scrolling::s_ScrollIfNecessary(ScreenInfo);
}
Expand Down
35 changes: 33 additions & 2 deletions src/host/consoleInformation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "..\types\inc\convert.hpp"

using Microsoft::Console::Interactivity::ServiceLocator;
using Microsoft::Console::Render::BlinkingState;
using Microsoft::Console::VirtualTerminal::VtIo;

CONSOLE_INFORMATION::CONSOLE_INFORMATION() :
Expand Down Expand Up @@ -222,7 +223,8 @@ InputBuffer* const CONSOLE_INFORMATION::GetActiveInputBuffer() const
// - the default foreground color of the console.
COLORREF CONSOLE_INFORMATION::GetDefaultForeground() const noexcept
{
return Settings::CalculateDefaultForeground();
const auto fg = GetDefaultForegroundColor();
return fg != INVALID_COLOR ? fg : GetColorTableEntry(LOBYTE(GetFillAttribute()) & FG_ATTRS);
}

// Method Description:
Expand All @@ -236,7 +238,25 @@ COLORREF CONSOLE_INFORMATION::GetDefaultForeground() const noexcept
// - the default background color of the console.
COLORREF CONSOLE_INFORMATION::GetDefaultBackground() const noexcept
{
return Settings::CalculateDefaultBackground();
const auto bg = GetDefaultBackgroundColor();
return bg != INVALID_COLOR ? bg : GetColorTableEntry((LOBYTE(GetFillAttribute()) & BG_ATTRS) >> 4);
}

// Method Description:
// - Get the colors of a particular text attribute, using our color table,
// and our configured default attributes.
// Arguments:
// - attr: the TextAttribute to retrieve the foreground color of.
// Return Value:
// - The color values of the attribute's foreground and background.
std::pair<COLORREF, COLORREF> CONSOLE_INFORMATION::LookupAttributeColors(const TextAttribute& attr) const noexcept
{
_blinkingState.RecordBlinkingUsage(attr);
return attr.CalculateRgbColors(Get256ColorTable(),
GetDefaultForeground(),
GetDefaultBackground(),
IsScreenReversed(),
_blinkingState.IsBlinkingFaint());
}

// Method Description:
Expand Down Expand Up @@ -355,6 +375,17 @@ Microsoft::Console::CursorBlinker& CONSOLE_INFORMATION::GetCursorBlinker() noexc
return _blinker;
}

// Method Description:
// - return a reference to the console's blinking state.
// Arguments:
// - <none>
// Return Value:
// - a reference to the console's blinking state.
BlinkingState& CONSOLE_INFORMATION::GetBlinkingState() const noexcept
{
return _blinkingState;
}

// Method Description:
// - Generates a CHAR_INFO for this output cell, using the TextAttribute
// GetLegacyAttributes method to generate the legacy style attributes.
Expand Down
4 changes: 4 additions & 0 deletions src/host/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Revision History:
#include "..\server\WaitQueue.h"

#include "..\host\RenderData.hpp"
#include "..\renderer\inc\BlinkingState.hpp"

// clang-format off
// Flags flags
Expand Down Expand Up @@ -125,6 +126,7 @@ class CONSOLE_INFORMATION :

COLORREF GetDefaultForeground() const noexcept;
COLORREF GetDefaultBackground() const noexcept;
std::pair<COLORREF, COLORREF> LookupAttributeColors(const TextAttribute& attr) const noexcept;

void SetTitle(const std::wstring_view newTitle);
void SetTitlePrefix(const std::wstring& newTitlePrefix);
Expand All @@ -141,6 +143,7 @@ class CONSOLE_INFORMATION :
friend class SCREEN_INFORMATION;
friend class CommonState;
Microsoft::Console::CursorBlinker& GetCursorBlinker() noexcept;
Microsoft::Console::Render::BlinkingState& GetBlinkingState() const noexcept;

CHAR_INFO AsCharInfo(const OutputCellView& cell) const noexcept;

Expand All @@ -157,6 +160,7 @@ class CONSOLE_INFORMATION :

Microsoft::Console::VirtualTerminal::VtIo _vtIo;
Microsoft::Console::CursorBlinker _blinker;
mutable Microsoft::Console::Render::BlinkingState _blinkingState;
};

#define ConsoleLocked() (ServiceLocator::LocateGlobals()->getConsoleInformation()->ConsoleLock.OwningThread == NtCurrentTeb()->ClientId.UniqueThread)
Expand Down
45 changes: 0 additions & 45 deletions src/host/settings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -831,51 +831,6 @@ bool Settings::GetUseDx() const noexcept
return _fUseDx;
}

// Method Description:
// - Return the default foreground color of the console. If the settings are
// configured to have a default foreground color (separate from the color
// table), this will return that value. Otherwise it will return the value
// from the colortable corresponding to our default legacy attributes.
// Arguments:
// - <none>
// Return Value:
// - the default foreground color of the console.
COLORREF Settings::CalculateDefaultForeground() const noexcept
{
const auto fg = GetDefaultForegroundColor();
return fg != INVALID_COLOR ? fg : GetColorTableEntry(LOBYTE(_wFillAttribute) & FG_ATTRS);
}

// Method Description:
// - Return the default background color of the console. If the settings are
// configured to have a default background color (separate from the color
// table), this will return that value. Otherwise it will return the value
// from the colortable corresponding to our default legacy attributes.
// Arguments:
// - <none>
// Return Value:
// - the default background color of the console.
COLORREF Settings::CalculateDefaultBackground() const noexcept
{
const auto bg = GetDefaultBackgroundColor();
return bg != INVALID_COLOR ? bg : GetColorTableEntry((LOBYTE(_wFillAttribute) & BG_ATTRS) >> 4);
}

// Method Description:
// - Get the colors of a particular text attribute, using our color table,
// and our configured default attributes.
// Arguments:
// - attr: the TextAttribute to retrieve the foreground color of.
// Return Value:
// - The color values of the attribute's foreground and background.
std::pair<COLORREF, COLORREF> Settings::LookupAttributeColors(const TextAttribute& attr) const noexcept
{
return attr.CalculateRgbColors(Get256ColorTable(),
CalculateDefaultForeground(),
CalculateDefaultBackground(),
_fScreenReversed);
}

bool Settings::GetCopyColor() const noexcept
{
return _fCopyColor;
Expand Down
Loading

0 comments on commit 825039e

Please sign in to comment.