2020-02-12 20:06:46 +01:00
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
# include "pch.h"
# include "TermControl.h"
# include <argb.h>
# include <DefaultSettings.h>
# include <unicode.hpp>
# include <Utf16Parser.hpp>
# include <Utils.h>
# include <WinUser.h>
# include <LibraryResources.h>
# include "..\..\types\inc\GlyphWidth.hpp"
# include "TermControl.g.cpp"
# include "TermControlAutomationPeer.h"
using namespace : : Microsoft : : Console : : Types ;
using namespace : : Microsoft : : Terminal : : Core ;
using namespace winrt : : Windows : : UI : : Xaml ;
using namespace winrt : : Windows : : UI : : Xaml : : Automation : : Peers ;
using namespace winrt : : Windows : : UI : : Core ;
using namespace winrt : : Windows : : System ;
using namespace winrt : : Microsoft : : Terminal : : Settings ;
using namespace winrt : : Windows : : ApplicationModel : : DataTransfer ;
namespace winrt : : Microsoft : : Terminal : : TerminalControl : : implementation
{
// Helper static function to ensure that all ambiguous-width glyphs are reported as narrow.
// See microsoft/terminal#2066 for more info.
static bool _IsGlyphWideForceNarrowFallback ( const std : : wstring_view /* glyph */ )
{
return false ; // glyph is not wide.
}
static bool _EnsureStaticInitialization ( )
{
// use C++11 magic statics to make sure we only do this once.
static bool initialized = [ ] ( ) {
// *** THIS IS A SINGLETON ***
SetGlyphWidthFallback ( _IsGlyphWideForceNarrowFallback ) ;
return true ;
} ( ) ;
return initialized ;
}
TermControl : : TermControl ( ) :
TermControl ( Settings : : TerminalSettings { } , TerminalConnection : : ITerminalConnection { nullptr } )
{
}
TermControl : : TermControl ( Settings : : IControlSettings settings , TerminalConnection : : ITerminalConnection connection ) :
_connection { connection } ,
_initializedTerminal { false } ,
_root { nullptr } ,
_swapChainPanel { nullptr } ,
_settings { settings } ,
_closing { false } ,
_isTerminalInitiatedScroll { false } ,
_autoScrollVelocity { 0 } ,
_autoScrollingPointerPoint { std : : nullopt } ,
_autoScrollTimer { } ,
_lastAutoScrollUpdateTime { std : : nullopt } ,
_desiredFont { DEFAULT_FONT_FACE , 0 , 10 , { 0 , DEFAULT_FONT_SIZE } , CP_UTF8 } ,
_actualFont { DEFAULT_FONT_FACE , 0 , 10 , { 0 , DEFAULT_FONT_SIZE } , CP_UTF8 , false } ,
_touchAnchor { std : : nullopt } ,
_cursorTimer { } ,
_lastMouseClick { } ,
_lastMouseClickPos { } ,
_searchBox { nullptr } ,
2020-02-20 19:30:10 +01:00
_tsfInputControl { nullptr } ,
_unfocusedClickPos { std : : nullopt } ,
_isClickDragSelection { false }
2020-02-12 20:06:46 +01:00
{
_EnsureStaticInitialization ( ) ;
_Create ( ) ;
}
void TermControl : : _Create ( )
{
Controls : : Grid container ;
Controls : : ColumnDefinition contentColumn { } ;
Controls : : ColumnDefinition scrollbarColumn { } ;
contentColumn . Width ( GridLength { 1.0 , GridUnitType : : Star } ) ;
scrollbarColumn . Width ( GridLength { 1.0 , GridUnitType : : Auto } ) ;
container . ColumnDefinitions ( ) . Append ( contentColumn ) ;
container . ColumnDefinitions ( ) . Append ( scrollbarColumn ) ;
_scrollBar = Controls : : Primitives : : ScrollBar { } ;
_scrollBar . Orientation ( Controls : : Orientation : : Vertical ) ;
_scrollBar . IndicatorMode ( Controls : : Primitives : : ScrollingIndicatorMode : : MouseIndicator ) ;
_scrollBar . HorizontalAlignment ( HorizontalAlignment : : Right ) ;
_scrollBar . VerticalAlignment ( VerticalAlignment : : Stretch ) ;
// Initialize the scrollbar with some placeholder values.
// The scrollbar will be updated with real values on _Initialize
_scrollBar . Maximum ( 1 ) ;
_scrollBar . ViewportSize ( 10 ) ;
_scrollBar . IsTabStop ( false ) ;
_scrollBar . SmallChange ( 1 ) ;
_scrollBar . LargeChange ( 4 ) ;
_scrollBar . Visibility ( Visibility : : Visible ) ;
_tsfInputControl = TSFInputControl ( ) ;
_tsfInputControl . CompositionCompleted ( { this , & TermControl : : _CompositionCompleted } ) ;
_tsfInputControl . CurrentCursorPosition ( { this , & TermControl : : _CurrentCursorPositionHandler } ) ;
_tsfInputControl . CurrentFontInfo ( { this , & TermControl : : _FontInfoHandler } ) ;
container . Children ( ) . Append ( _tsfInputControl ) ;
// Create the SwapChainPanel that will display our content
Controls : : SwapChainPanel swapChainPanel ;
_sizeChangedRevoker = swapChainPanel . SizeChanged ( winrt : : auto_revoke , { this , & TermControl : : _SwapChainSizeChanged } ) ;
_compositionScaleChangedRevoker = swapChainPanel . CompositionScaleChanged ( winrt : : auto_revoke , { this , & TermControl : : _SwapChainScaleChanged } ) ;
// Initialize the terminal only once the swapchainpanel is loaded - that
// way, we'll be able to query the real pixel size it got on layout
_layoutUpdatedRevoker = swapChainPanel . LayoutUpdated ( winrt : : auto_revoke , [ this ] ( auto /*s*/ , auto /*e*/ ) {
// This event fires every time the layout changes, but it is always the last one to fire
// in any layout change chain. That gives us great flexibility in finding the right point
// at which to initialize our renderer (and our terminal).
// Any earlier than the last layout update and we may not know the terminal's starting size.
if ( _InitializeTerminal ( ) )
{
// Only let this succeed once.
_layoutUpdatedRevoker . revoke ( ) ;
}
} ) ;
container . Children ( ) . Append ( swapChainPanel ) ;
container . Children ( ) . Append ( _scrollBar ) ;
Controls : : Grid : : SetColumn ( swapChainPanel , 0 ) ;
Controls : : Grid : : SetColumn ( _scrollBar , 1 ) ;
Controls : : Grid root { } ;
Controls : : Image bgImageLayer { } ;
root . Children ( ) . Append ( bgImageLayer ) ;
root . Children ( ) . Append ( container ) ;
_root = root ;
_bgImageLayer = bgImageLayer ;
_swapChainPanel = swapChainPanel ;
this - > Content ( _root ) ;
_ApplyUISettings ( ) ;
// These are important:
// 1. When we get tapped, focus us
_tappedRevoker = this - > Tapped ( winrt : : auto_revoke , [ this ] ( auto & , auto & e ) {
Focus ( FocusState : : Pointer ) ;
e . Handled ( true ) ;
} ) ;
// 2. Make sure we can be focused (why this isn't `Focusable` I'll never know)
this - > IsTabStop ( true ) ;
// 3. Actually not sure about this one. Maybe it isn't necessary either.
this - > AllowFocusOnInteraction ( true ) ;
// DON'T CALL _InitializeTerminal here - wait until the swap chain is loaded to do that.
// Subscribe to the connection's disconnected event and call our connection closed handlers.
_connectionStateChangedRevoker = _connection . StateChanged ( winrt : : auto_revoke , [ this ] ( auto & & /*s*/ , auto & & /*v*/ ) {
_ConnectionStateChangedHandlers ( * this , nullptr ) ;
} ) ;
_root . AllowDrop ( true ) ;
_root . Drop ( { get_weak ( ) , & TermControl : : _DragDropHandler } ) ;
_root . DragOver ( { get_weak ( ) , & TermControl : : _DragOverHandler } ) ;
}
// Method Description:
// - Create the SearchBoxControl object, and attach it
// to the Terminal Control root
// Arguments:
// - <none>
// Return Value:
// - <none>
void TermControl : : CreateSearchBoxControl ( )
{
if ( ! _searchBox )
{
_searchBox = winrt : : make_self < SearchBoxControl > ( ) ;
_searchBox - > HorizontalAlignment ( HorizontalAlignment : : Right ) ;
_searchBox - > VerticalAlignment ( VerticalAlignment : : Top ) ;
// We need to make sure the searchbox does not overlap
// with the scroll bar
Thickness searchBoxPadding = { 0 , 0 , _scrollBar . ActualWidth ( ) , 0 } ;
_searchBox - > Margin ( searchBoxPadding ) ;
_root . Children ( ) . Append ( * _searchBox ) ;
// Event handlers
_searchBox - > Search ( { get_weak ( ) , & TermControl : : _Search } ) ;
_searchBox - > Closed ( { get_weak ( ) , & TermControl : : _CloseSearchBoxControl } ) ;
}
_searchBox - > SetFocusOnTextbox ( ) ;
}
// Method Description:
// - Search text in text buffer. This is triggered if the user click
// search button or press enter.
// Arguments:
// - text: the text to search
// - goForward: boolean that represents if the current search direction is forward
// - caseSensitive: boolean that represents if the current search is case sensitive
// Return Value:
// - <none>
void TermControl : : _Search ( const winrt : : hstring & text , const bool goForward , const bool caseSensitive )
{
if ( text . size ( ) = = 0 )
{
return ;
}
const Search : : Direction direction = goForward ?
Search : : Direction : : Forward :
Search : : Direction : : Backward ;
const Search : : Sensitivity sensitivity = caseSensitive ?
Search : : Sensitivity : : CaseSensitive :
Search : : Sensitivity : : CaseInsensitive ;
Search search ( * GetUiaData ( ) , text . c_str ( ) , direction , sensitivity ) ;
auto lock = _terminal - > LockForWriting ( ) ;
if ( search . FindNext ( ) )
{
_terminal - > SetBoxSelection ( false ) ;
search . Select ( ) ;
_renderer - > TriggerSelection ( ) ;
}
}
// Method Description:
// - The handler for the close button or pressing "Esc" when focusing on the
// search dialog.
// This removes the SearchBoxControl object from the XAML tree,
// reset smart pointer and set focus back to Terminal
// Arguments:
// - IInspectable: not used
// - RoutedEventArgs: not used
// Return Value:
// - <none>
void TermControl : : _CloseSearchBoxControl ( const winrt : : Windows : : Foundation : : IInspectable & /*sender*/ , RoutedEventArgs const & /*args*/ )
{
unsigned int idx ;
_root . Children ( ) . IndexOf ( * _searchBox , idx ) ;
_root . Children ( ) . RemoveAt ( idx ) ;
_searchBox = nullptr ;
// Set focus back to terminal control
this - > Focus ( FocusState : : Programmatic ) ;
}
// Method Description:
// - Given new settings for this profile, applies the settings to the current terminal.
// Arguments:
// - newSettings: New settings values for the profile in this terminal.
// Return Value:
// - <none>
winrt : : fire_and_forget TermControl : : UpdateSettings ( Settings : : IControlSettings newSettings )
{
_settings = newSettings ;
auto weakThis { get_weak ( ) } ;
// Dispatch a call to the UI thread to apply the new settings to the
// terminal.
co_await winrt : : resume_foreground ( _root . Dispatcher ( ) ) ;
// If 'weakThis' is locked, then we can safely work with 'this'
if ( auto control { weakThis . get ( ) } )
{
// Update our control settings
_ApplyUISettings ( ) ;
// Update DxEngine's SelectionBackground
_renderEngine - > SetSelectionBackground ( _settings . SelectionBackground ( ) ) ;
// Update the terminal core with its new Core settings
_terminal - > UpdateSettings ( _settings ) ;
// Refresh our font with the renderer
_UpdateFont ( ) ;
const auto width = _swapChainPanel . ActualWidth ( ) ;
const auto height = _swapChainPanel . ActualHeight ( ) ;
if ( width ! = 0 & & height ! = 0 )
{
// If the font size changed, or the _swapchainPanel's size changed
// for any reason, we'll need to make sure to also resize the
// buffer. _DoResize will invalidate everything for us.
auto lock = _terminal - > LockForWriting ( ) ;
_DoResize ( width , height ) ;
}
// set TSF Foreground
Media : : SolidColorBrush foregroundBrush { } ;
foregroundBrush . Color ( ColorRefToColor ( _settings . DefaultForeground ( ) ) ) ;
_tsfInputControl . Foreground ( foregroundBrush ) ;
}
}
// Method Description:
// - Style our UI elements based on the values in our _settings, and set up
// other control-specific settings. This method will be called whenever
// the settings are reloaded.
// * Calls _InitializeBackgroundBrush to set up the Xaml brush responsible
// for the control's background
// * Calls _BackgroundColorChanged to style the background of the control
// - Core settings will be passed to the terminal in _InitializeTerminal
// Arguments:
// - <none>
// Return Value:
// - <none>
void TermControl : : _ApplyUISettings ( )
{
_InitializeBackgroundBrush ( ) ;
uint32_t bg = _settings . DefaultBackground ( ) ;
_BackgroundColorChanged ( bg ) ;
// Apply padding as swapChainPanel's margin
auto newMargin = _ParseThicknessFromPadding ( _settings . Padding ( ) ) ;
auto existingMargin = _swapChainPanel . Margin ( ) ;
_swapChainPanel . Margin ( newMargin ) ;
// Initialize our font information.
const auto fontFace = _settings . FontFace ( ) ;
const short fontHeight = gsl : : narrow < short > ( _settings . FontSize ( ) ) ;
// The font width doesn't terribly matter, we'll only be using the
// height to look it up
// The other params here also largely don't matter.
// The family is only used to determine if the font is truetype or
// not, but DX doesn't use that info at all.
// The Codepage is additionally not actually used by the DX engine at all.
_actualFont = { fontFace , 0 , 10 , { 0 , fontHeight } , CP_UTF8 , false } ;
_desiredFont = { _actualFont } ;
// set TSF Foreground
Media : : SolidColorBrush foregroundBrush { } ;
foregroundBrush . Color ( ColorRefToColor ( _settings . DefaultForeground ( ) ) ) ;
_tsfInputControl . Foreground ( foregroundBrush ) ;
_tsfInputControl . Margin ( newMargin ) ;
// set number of rows to scroll at a time
_rowsToScroll = _settings . RowsToScroll ( ) ;
}
// Method Description:
// - Set up each layer's brush used to display the control's background.
// - Respects the settings for acrylic, background image and opacity from
// _settings.
// * If acrylic is not enabled, setup a solid color background, otherwise
// use bgcolor as acrylic's tint
// - Avoids image flickering and acrylic brush redraw if settings are changed
// but the appropriate brush is still in place.
// - Does not apply background color outside of acrylic mode;
// _BackgroundColorChanged must be called to do so.
// Arguments:
// - <none>
// Return Value:
// - <none>
void TermControl : : _InitializeBackgroundBrush ( )
{
if ( _settings . UseAcrylic ( ) )
{
// See if we've already got an acrylic background brush
// to avoid the flicker when setting up a new one
auto acrylic = _root . Background ( ) . try_as < Media : : AcrylicBrush > ( ) ;
// Instantiate a brush if there's not already one there
if ( acrylic = = nullptr )
{
acrylic = Media : : AcrylicBrush { } ;
acrylic . BackgroundSource ( Media : : AcrylicBackgroundSource : : HostBackdrop ) ;
}
// see GH#1082: Initialize background color so we don't get a
// fade/flash when _BackgroundColorChanged is called
uint32_t color = _settings . DefaultBackground ( ) ;
winrt : : Windows : : UI : : Color bgColor { } ;
bgColor . R = GetRValue ( color ) ;
bgColor . G = GetGValue ( color ) ;
bgColor . B = GetBValue ( color ) ;
bgColor . A = 255 ;
acrylic . FallbackColor ( bgColor ) ;
acrylic . TintColor ( bgColor ) ;
// Apply brush settings
acrylic . TintOpacity ( _settings . TintOpacity ( ) ) ;
// Apply brush to control if it's not already there
if ( _root . Background ( ) ! = acrylic )
{
_root . Background ( acrylic ) ;
}
}
else
{
Media : : SolidColorBrush solidColor { } ;
_root . Background ( solidColor ) ;
}
if ( ! _settings . BackgroundImage ( ) . empty ( ) )
{
Windows : : Foundation : : Uri imageUri { _settings . BackgroundImage ( ) } ;
// Check if the image brush is already pointing to the image
// in the modified settings; if it isn't (or isn't there),
// set a new image source for the brush
auto imageSource = _bgImageLayer . Source ( ) . try_as < Media : : Imaging : : BitmapImage > ( ) ;
if ( imageSource = = nullptr | |
imageSource . UriSource ( ) = = nullptr | |
imageSource . UriSource ( ) . RawUri ( ) ! = imageUri . RawUri ( ) )
{
// Note that BitmapImage handles the image load asynchronously,
// which is especially important since the image
// may well be both large and somewhere out on the
// internet.
Media : : Imaging : : BitmapImage image ( imageUri ) ;
_bgImageLayer . Source ( image ) ;
}
// Apply stretch, opacity and alignment settings
_bgImageLayer . Stretch ( _settings . BackgroundImageStretchMode ( ) ) ;
_bgImageLayer . Opacity ( _settings . BackgroundImageOpacity ( ) ) ;
_bgImageLayer . HorizontalAlignment ( _settings . BackgroundImageHorizontalAlignment ( ) ) ;
_bgImageLayer . VerticalAlignment ( _settings . BackgroundImageVerticalAlignment ( ) ) ;
}
else
{
_bgImageLayer . Source ( nullptr ) ;
}
}
// Method Description:
// - Style the background of the control with the provided background color
// Arguments:
// - color: The background color to use as a uint32 (aka DWORD COLORREF)
// Return Value:
// - <none>
winrt : : fire_and_forget TermControl : : _BackgroundColorChanged ( const uint32_t color )
{
auto weakThis { get_weak ( ) } ;
co_await winrt : : resume_foreground ( _root . Dispatcher ( ) ) ;
if ( auto control { weakThis . get ( ) } )
{
const auto R = GetRValue ( color ) ;
const auto G = GetGValue ( color ) ;
const auto B = GetBValue ( color ) ;
winrt : : Windows : : UI : : Color bgColor { } ;
bgColor . R = R ;
bgColor . G = G ;
bgColor . B = B ;
bgColor . A = 255 ;
if ( auto acrylic = _root . Background ( ) . try_as < Media : : AcrylicBrush > ( ) )
{
acrylic . FallbackColor ( bgColor ) ;
acrylic . TintColor ( bgColor ) ;
}
else if ( auto solidColor = _root . Background ( ) . try_as < Media : : SolidColorBrush > ( ) )
{
solidColor . Color ( bgColor ) ;
}
// Set the default background as transparent to prevent the
// DX layer from overwriting the background image or acrylic effect
_settings . DefaultBackground ( ARGB ( 0 , R , G , B ) ) ;
}
}
TermControl : : ~ TermControl ( )
{
Close ( ) ;
}
// Method Description:
// - Creates an automation peer for the Terminal Control, enabling accessibility on our control.
// Arguments:
// - None
// Return Value:
// - The automation peer for our control
Windows : : UI : : Xaml : : Automation : : Peers : : AutomationPeer TermControl : : OnCreateAutomationPeer ( )
try
{
if ( GetUiaData ( ) )
{
// create a custom automation peer with this code pattern:
// (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers)
auto autoPeer = winrt : : make_self < winrt : : Microsoft : : Terminal : : TerminalControl : : implementation : : TermControlAutomationPeer > ( this ) ;
_uiaEngine = std : : make_unique < : : Microsoft : : Console : : Render : : UiaEngine > ( autoPeer . get ( ) ) ;
_renderer - > AddRenderEngine ( _uiaEngine . get ( ) ) ;
return * autoPeer ;
}
return nullptr ;
}
catch ( . . . )
{
LOG_CAUGHT_EXCEPTION ( ) ;
return nullptr ;
}
: : Microsoft : : Console : : Types : : IUiaData * TermControl : : GetUiaData ( ) const
{
return _terminal . get ( ) ;
}
const FontInfo TermControl : : GetActualFont ( ) const
{
return _actualFont ;
}
const Windows : : UI : : Xaml : : Thickness TermControl : : GetPadding ( ) const
{
return _swapChainPanel . Margin ( ) ;
}
TerminalConnection : : ConnectionState TermControl : : ConnectionState ( ) const
{
return _connection . State ( ) ;
}
winrt : : fire_and_forget TermControl : : SwapChainChanged ( )
{
if ( ! _initializedTerminal )
{
return ;
}
auto chain = _renderEngine - > GetSwapChain ( ) ;
auto weakThis { get_weak ( ) } ;
co_await winrt : : resume_foreground ( _swapChainPanel . Dispatcher ( ) ) ;
// If 'weakThis' is locked, then we can safely work with 'this'
if ( auto control { weakThis . get ( ) } )
{
auto lock = _terminal - > LockForWriting ( ) ;
auto nativePanel = _swapChainPanel . as < ISwapChainPanelNative > ( ) ;
nativePanel - > SetSwapChain ( chain . Get ( ) ) ;
}
}
winrt : : fire_and_forget TermControl : : _SwapChainRoutine ( )
{
auto chain = _renderEngine - > GetSwapChain ( ) ;
auto weakThis { get_weak ( ) } ;
co_await winrt : : resume_foreground ( _swapChainPanel . Dispatcher ( ) ) ;
if ( auto control { weakThis . get ( ) } )
{
_terminal - > LockConsole ( ) ;
auto nativePanel = _swapChainPanel . as < ISwapChainPanelNative > ( ) ;
nativePanel - > SetSwapChain ( chain . Get ( ) ) ;
_terminal - > UnlockConsole ( ) ;
}
}
bool TermControl : : _InitializeTerminal ( )
{
if ( _initializedTerminal )
{
return false ;
}
const auto windowWidth = _swapChainPanel . ActualWidth ( ) ; // Width() and Height() are NaN?
const auto windowHeight = _swapChainPanel . ActualHeight ( ) ;
if ( windowWidth = = 0 | | windowHeight = = 0 )
{
return false ;
}
_terminal = std : : make_unique < : : Microsoft : : Terminal : : Core : : Terminal > ( ) ;
// First create the render thread.
// Then stash a local pointer to the render thread so we can initialize it and enable it
// to paint itself *after* we hand off its ownership to the renderer.
// We split up construction and initialization of the render thread object this way
// because the renderer and render thread have circular references to each other.
auto renderThread = std : : make_unique < : : Microsoft : : Console : : Render : : RenderThread > ( ) ;
auto * const localPointerToThread = renderThread . get ( ) ;
// Now create the renderer and initialize the render thread.
_renderer = std : : make_unique < : : Microsoft : : Console : : Render : : Renderer > ( _terminal . get ( ) , nullptr , 0 , std : : move ( renderThread ) ) ;
: : Microsoft : : Console : : Render : : IRenderTarget & renderTarget = * _renderer ;
THROW_IF_FAILED ( localPointerToThread - > Initialize ( _renderer . get ( ) ) ) ;
// Set up the DX Engine
auto dxEngine = std : : make_unique < : : Microsoft : : Console : : Render : : DxEngine > ( ) ;
_renderer - > AddRenderEngine ( dxEngine . get ( ) ) ;
// Initialize our font with the renderer
// We don't have to care about DPI. We'll get a change message immediately if it's not 96
// and react accordingly.
_UpdateFont ( true ) ;
const COORD windowSize { static_cast < short > ( windowWidth ) , static_cast < short > ( windowHeight ) } ;
// Fist set up the dx engine with the window size in pixels.
// Then, using the font, get the number of characters that can fit.
// Resize our terminal connection to match that size, and initialize the terminal with that size.
const auto viewInPixels = Viewport : : FromDimensions ( { 0 , 0 } , windowSize ) ;
THROW_IF_FAILED ( dxEngine - > SetWindowSize ( { viewInPixels . Width ( ) , viewInPixels . Height ( ) } ) ) ;
// Update DxEngine's SelectionBackground
dxEngine - > SetSelectionBackground ( _settings . SelectionBackground ( ) ) ;
const auto vp = dxEngine - > GetViewportInCharacters ( viewInPixels ) ;
const auto width = vp . Width ( ) ;
const auto height = vp . Height ( ) ;
_connection . Resize ( height , width ) ;
// Override the default width and height to match the size of the swapChainPanel
_settings . InitialCols ( width ) ;
_settings . InitialRows ( height ) ;
_terminal - > CreateFromSettings ( _settings , renderTarget ) ;
// Tell the DX Engine to notify us when the swap chain changes.
dxEngine - > SetCallback ( std : : bind ( & TermControl : : SwapChainChanged , this ) ) ;
// TODO:GH#3927 - Make it possible to hot-reload this setting. Right
// here, the setting will only be used when the Terminal is initialized.
dxEngine - > SetRetroTerminalEffects ( _settings . RetroTerminalEffect ( ) ) ;
THROW_IF_FAILED ( dxEngine - > Enable ( ) ) ;
_renderEngine = std : : move ( dxEngine ) ;
// This event is explicitly revoked in the destructor: does not need weak_ref
auto onReceiveOutputFn = [ this ] ( const hstring str ) {
_terminal - > Write ( str ) ;
} ;
_connectionOutputEventToken = _connection . TerminalOutput ( onReceiveOutputFn ) ;
auto inputFn = std : : bind ( & TermControl : : _SendInputToConnection , this , std : : placeholders : : _1 ) ;
_terminal - > SetWriteInputCallback ( inputFn ) ;
_SwapChainRoutine ( ) ;
// Set up the height of the ScrollViewer and the grid we're using to fake our scrolling height
auto bottom = _terminal - > GetViewport ( ) . BottomExclusive ( ) ;
auto bufferHeight = bottom ;
const auto originalMaximum = _scrollBar . Maximum ( ) ;
const auto originalMinimum = _scrollBar . Minimum ( ) ;
const auto originalValue = _scrollBar . Value ( ) ;
const auto originalViewportSize = _scrollBar . ViewportSize ( ) ;
_scrollBar . Maximum ( bufferHeight - bufferHeight ) ;
_scrollBar . Minimum ( 0 ) ;
_scrollBar . Value ( 0 ) ;
_scrollBar . ViewportSize ( bufferHeight ) ;
_scrollBar . ValueChanged ( { this , & TermControl : : _ScrollbarChangeHandler } ) ;
_scrollBar . PointerPressed ( { this , & TermControl : : _CapturePointer } ) ;
_scrollBar . PointerReleased ( { this , & TermControl : : _ReleasePointerCapture } ) ;
// Apply settings for scrollbar
if ( _settings . ScrollState ( ) = = ScrollbarState : : Visible )
{
_scrollBar . IndicatorMode ( Controls : : Primitives : : ScrollingIndicatorMode : : MouseIndicator ) ;
}
else if ( _settings . ScrollState ( ) = = ScrollbarState : : Hidden )
{
_scrollBar . IndicatorMode ( Controls : : Primitives : : ScrollingIndicatorMode : : None ) ;
// In the scenario where the user has turned off the OS setting to automatically hide scollbars, the
// Terminal scrollbar would still be visible; so, we need to set the control's visibility accordingly to
// achieve the intended effect.
_scrollBar . Visibility ( Visibility : : Collapsed ) ;
}
else
{
// Default behavior
_scrollBar . IndicatorMode ( Controls : : Primitives : : ScrollingIndicatorMode : : MouseIndicator ) ;
}
_root . PointerWheelChanged ( { this , & TermControl : : _MouseWheelHandler } ) ;
// These need to be hooked up to the SwapChainPanel because we don't want the scrollbar to respond to pointer events (GitHub #950)
_swapChainPanel . PointerPressed ( { this , & TermControl : : _PointerPressedHandler } ) ;
_swapChainPanel . PointerMoved ( { this , & TermControl : : _PointerMovedHandler } ) ;
_swapChainPanel . PointerReleased ( { this , & TermControl : : _PointerReleasedHandler } ) ;
localPointerToThread - > EnablePainting ( ) ;
// No matter what order these guys are in, The KeyDown's will fire
// before the CharacterReceived, so we can't easily get characters
// first, then fallback to getting keys from vkeys.
// TODO: This apparently handles keys and characters correctly, though
// I'd keep an eye on it, and test more.
// I presume that the characters that aren't translated by terminalInput
// just end up getting ignored, and the rest of the input comes
// through CharacterReceived.
// I don't believe there's a difference between KeyDown and
// PreviewKeyDown for our purposes
// These two handlers _must_ be on this, not _root.
this - > PreviewKeyDown ( { this , & TermControl : : _KeyDownHandler } ) ;
this - > CharacterReceived ( { this , & TermControl : : _CharacterHandler } ) ;
auto pfnTitleChanged = std : : bind ( & TermControl : : _TerminalTitleChanged , this , std : : placeholders : : _1 ) ;
_terminal - > SetTitleChangedCallback ( pfnTitleChanged ) ;
auto pfnBackgroundColorChanged = std : : bind ( & TermControl : : _BackgroundColorChanged , this , std : : placeholders : : _1 ) ;
_terminal - > SetBackgroundCallback ( pfnBackgroundColorChanged ) ;
auto pfnScrollPositionChanged = std : : bind ( & TermControl : : _TerminalScrollPositionChanged , this , std : : placeholders : : _1 , std : : placeholders : : _2 , std : : placeholders : : _3 ) ;
_terminal - > SetScrollPositionChangedCallback ( pfnScrollPositionChanged ) ;
static constexpr auto AutoScrollUpdateInterval = std : : chrono : : microseconds ( static_cast < int > ( 1.0 / 30.0 * 1000000 ) ) ;
_autoScrollTimer . Interval ( AutoScrollUpdateInterval ) ;
_autoScrollTimer . Tick ( { get_weak ( ) , & TermControl : : _UpdateAutoScroll } ) ;
// Set up blinking cursor
int blinkTime = GetCaretBlinkTime ( ) ;
if ( blinkTime ! = INFINITE )
{
// Create a timer
_cursorTimer = std : : make_optional ( DispatcherTimer ( ) ) ;
_cursorTimer . value ( ) . Interval ( std : : chrono : : milliseconds ( blinkTime ) ) ;
_cursorTimer . value ( ) . Tick ( { get_weak ( ) , & TermControl : : _BlinkCursor } ) ;
_cursorTimer . value ( ) . Start ( ) ;
}
else
{
// The user has disabled cursor blinking
_cursorTimer = std : : nullopt ;
}
// import value from WinUser (convert from milli-seconds to micro-seconds)
_multiClickTimer = GetDoubleClickTime ( ) * 1000 ;
_gotFocusRevoker = this - > GotFocus ( winrt : : auto_revoke , { this , & TermControl : : _GotFocusHandler } ) ;
_lostFocusRevoker = this - > LostFocus ( winrt : : auto_revoke , { this , & TermControl : : _LostFocusHandler } ) ;
// Focus the control here. If we do it up above (in _Create_), then the
// focus won't actually get passed to us. I believe this is because
// we're not technically a part of the UI tree yet, so focusing us
// becomes a no-op.
this - > Focus ( FocusState : : Programmatic ) ;
_connection . Start ( ) ;
_initializedTerminal = true ;
_InitializedHandlers ( * this , nullptr ) ;
return true ;
}
void TermControl : : _CharacterHandler ( winrt : : Windows : : Foundation : : IInspectable const & /*sender*/ ,
Input : : CharacterReceivedRoutedEventArgs const & e )
{
if ( _closing )
{
return ;
}
const auto ch = e . Character ( ) ;
const bool handled = _terminal - > SendCharEvent ( ch ) ;
e . Handled ( handled ) ;
}
void TermControl : : _KeyDownHandler ( winrt : : Windows : : Foundation : : IInspectable const & /*sender*/ ,
Input : : KeyRoutedEventArgs const & e )
{
// If the current focused element is a child element of searchbox,
// we do not send this event up to terminal
if ( _searchBox & & _searchBox - > ContainsFocus ( ) )
{
return ;
}
// mark event as handled and do nothing if...
// - closing
// - key modifier is pressed
// NOTE: for key combos like CTRL + C, two events are fired (one for CTRL, one for 'C'). We care about the 'C' event and then check for key modifiers below.
if ( _closing | |
e . OriginalKey ( ) = = VirtualKey : : Control | |
e . OriginalKey ( ) = = VirtualKey : : Shift | |
e . OriginalKey ( ) = = VirtualKey : : Menu | |
e . OriginalKey ( ) = = VirtualKey : : LeftWindows | |
e . OriginalKey ( ) = = VirtualKey : : RightWindows )
{
e . Handled ( true ) ;
return ;
}
const auto modifiers = _GetPressedModifierKeys ( ) ;
const auto vkey = gsl : : narrow_cast < WORD > ( e . OriginalKey ( ) ) ;
const auto scanCode = gsl : : narrow_cast < WORD > ( e . KeyStatus ( ) . ScanCode ) ;
bool handled = false ;
// GH#2235: Terminal::Settings hasn't been modified to differentiate between AltGr and Ctrl+Alt yet.
// -> Don't check for key bindings if this is an AltGr key combination.
if ( ! modifiers . IsAltGrPressed ( ) )
{
auto bindings = _settings . KeyBindings ( ) ;
if ( bindings )
{
handled = bindings . TryKeyChord ( {
modifiers . IsCtrlPressed ( ) ,
modifiers . IsAltPressed ( ) ,
modifiers . IsShiftPressed ( ) ,
vkey ,
} ) ;
}
}
if ( ! handled )
{
handled = _TrySendKeyEvent ( vkey , scanCode , modifiers ) ;
}
// Manually prevent keyboard navigation with tab. We want to send tab to
// the terminal, and we don't want to be able to escape focus of the
// control with tab.
if ( e . OriginalKey ( ) = = VirtualKey : : Tab )
{
handled = true ;
}
e . Handled ( handled ) ;
}
// Method Description:
// - Send this particular key event to the terminal.
// See Terminal::SendKeyEvent for more information.
// - Clears the current selection.
// - Makes the cursor briefly visible during typing.
// Arguments:
// - vkey: The vkey of the key pressed.
// - states: The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states.
bool TermControl : : _TrySendKeyEvent ( const WORD vkey , const WORD scanCode , const ControlKeyStates modifiers )
{
// When there is a selection active, escape should clear it and NOT flow through
// to the terminal. With any other keypress, it should clear the selection AND
// flow through to the terminal.
if ( _terminal - > IsSelectionActive ( ) )
{
_terminal - > ClearSelection ( ) ;
if ( vkey = = VK_ESCAPE )
{
return true ;
}
}
// If the terminal translated the key, mark the event as handled.
// This will prevent the system from trying to get the character out
// of it and sending us a CharacterReceived event.
const auto handled = vkey ? _terminal - > SendKeyEvent ( vkey , scanCode , modifiers ) : true ;
if ( _cursorTimer . has_value ( ) )
{
// Manually show the cursor when a key is pressed. Restarting
// the timer prevents flickering.
_terminal - > SetCursorVisible ( true ) ;
_cursorTimer . value ( ) . Start ( ) ;
}
return handled ;
}
// Method Description:
// - handle a mouse click event. Begin selection process.
// Arguments:
// - sender: the XAML element responding to the pointer input
// - args: event data
void TermControl : : _PointerPressedHandler ( Windows : : Foundation : : IInspectable const & sender ,
Input : : PointerRoutedEventArgs const & args )
{
_CapturePointer ( sender , args ) ;
const auto ptr = args . Pointer ( ) ;
const auto point = args . GetCurrentPoint ( _root ) ;
2020-02-13 01:32:50 +01:00
if ( ! _focused )
2020-02-12 20:06:46 +01:00
{
2020-02-13 01:32:50 +01:00
Focus ( FocusState : : Pointer ) ;
2020-02-12 20:06:46 +01:00
2020-02-13 01:32:50 +01:00
// Save the click position here when the terminal does not have focus
// because they might be performing a click-drag selection. Since we
// only want to start the selection when the user moves the pointer with
// the left mouse button held down, the PointerMovedHandler will use
// this saved position to set the SelectionAnchor.
2020-02-20 19:30:10 +01:00
_unfocusedClickPos = point . Position ( ) ;
2020-02-13 01:32:50 +01:00
}
if ( ptr . PointerDeviceType ( ) = = Windows : : Devices : : Input : : PointerDeviceType : : Mouse | | ptr . PointerDeviceType ( ) = = Windows : : Devices : : Input : : PointerDeviceType : : Pen )
{
2020-02-12 20:06:46 +01:00
const auto modifiers = static_cast < uint32_t > ( args . KeyModifiers ( ) ) ;
// static_cast to a uint32_t because we can't use the WI_IsFlagSet
// macro directly with a VirtualKeyModifiers
const auto altEnabled = WI_IsFlagSet ( modifiers , static_cast < uint32_t > ( VirtualKeyModifiers : : Menu ) ) ;
const auto shiftEnabled = WI_IsFlagSet ( modifiers , static_cast < uint32_t > ( VirtualKeyModifiers : : Shift ) ) ;
if ( point . Properties ( ) . IsLeftButtonPressed ( ) )
{
2020-02-20 19:30:10 +01:00
// _unfocusedClickPos having a value signifies to us that
2020-02-13 01:32:50 +01:00
// the user clicked on an unfocused terminal. We don't want
// a single left click from out of focus to start a selection,
// so we return fast here.
2020-02-20 19:30:10 +01:00
if ( _unfocusedClickPos )
2020-02-13 01:32:50 +01:00
{
args . Handled ( true ) ;
return ;
}
2020-02-12 20:06:46 +01:00
const auto cursorPosition = point . Position ( ) ;
const auto terminalPosition = _GetTerminalPosition ( cursorPosition ) ;
// handle ALT key
_terminal - > SetBoxSelection ( altEnabled ) ;
auto clickCount = _NumberOfClicks ( cursorPosition , point . Timestamp ( ) ) ;
// This formula enables the number of clicks to cycle properly between single-, double-, and triple-click.
// To increase the number of acceptable click states, simply increment MAX_CLICK_COUNT and add another if-statement
const unsigned int MAX_CLICK_COUNT = 3 ;
const auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ( ( clickCount + MAX_CLICK_COUNT - 1 ) % MAX_CLICK_COUNT ) + 1 : clickCount ;
if ( multiClickMapper = = 3 )
{
_terminal - > TripleClickSelection ( terminalPosition ) ;
}
else if ( multiClickMapper = = 2 )
{
_terminal - > DoubleClickSelection ( terminalPosition ) ;
}
else
{
if ( shiftEnabled & & _terminal - > IsSelectionActive ( ) )
{
_terminal - > SetEndSelectionPosition ( terminalPosition ) ;
}
else
{
// save location before rendering
_terminal - > SetSelectionAnchor ( terminalPosition ) ;
}
_lastMouseClick = point . Timestamp ( ) ;
_lastMouseClickPos = cursorPosition ;
}
_renderer - > TriggerSelection ( ) ;
}
else if ( point . Properties ( ) . IsRightButtonPressed ( ) )
{
// copyOnSelect causes right-click to always paste
if ( _terminal - > IsCopyOnSelectActive ( ) | | ! _terminal - > IsSelectionActive ( ) )
{
PasteTextFromClipboard ( ) ;
}
else
{
CopySelectionToClipboard ( ! shiftEnabled ) ;
}
}
}
else if ( ptr . PointerDeviceType ( ) = = Windows : : Devices : : Input : : PointerDeviceType : : Touch )
{
const auto contactRect = point . Properties ( ) . ContactRect ( ) ;
// Set our touch rect, to start a pan.
_touchAnchor = winrt : : Windows : : Foundation : : Point { contactRect . X , contactRect . Y } ;
}
args . Handled ( true ) ;
}
// Method Description:
// - handle a mouse moved event. Specifically handling mouse drag to update selection process.
// Arguments:
// - sender: not used
// - args: event data
void TermControl : : _PointerMovedHandler ( Windows : : Foundation : : IInspectable const & /*sender*/ ,
Input : : PointerRoutedEventArgs const & args )
{
const auto ptr = args . Pointer ( ) ;
const auto point = args . GetCurrentPoint ( _root ) ;
if ( ptr . PointerDeviceType ( ) = = Windows : : Devices : : Input : : PointerDeviceType : : Mouse | | ptr . PointerDeviceType ( ) = = Windows : : Devices : : Input : : PointerDeviceType : : Pen )
{
if ( point . Properties ( ) . IsLeftButtonPressed ( ) )
{
2020-02-20 19:30:10 +01:00
_isClickDragSelection = true ;
2020-02-13 01:32:50 +01:00
// If this does not have a value, it means that PointerPressedHandler already
// set the SelectionAnchor. If it does have a value, that means the user is
// performing a click-drag selection on an unfocused terminal, so
// a SelectionAnchor isn't set yet. We'll have to set it here.
2020-02-20 19:30:10 +01:00
if ( _unfocusedClickPos )
2020-02-13 01:32:50 +01:00
{
2020-02-20 19:30:10 +01:00
_terminal - > SetSelectionAnchor ( _GetTerminalPosition ( * _unfocusedClickPos ) ) ;
2020-02-13 01:32:50 +01:00
}
2020-02-12 20:06:46 +01:00
const auto cursorPosition = point . Position ( ) ;
_SetEndSelectionPointAtCursor ( cursorPosition ) ;
const double cursorBelowBottomDist = cursorPosition . Y - _swapChainPanel . Margin ( ) . Top - _swapChainPanel . ActualHeight ( ) ;
const double cursorAboveTopDist = - 1 * cursorPosition . Y + _swapChainPanel . Margin ( ) . Top ;
constexpr double MinAutoScrollDist = 2.0 ; // Arbitrary value
double newAutoScrollVelocity = 0.0 ;
if ( cursorBelowBottomDist > MinAutoScrollDist )
{
newAutoScrollVelocity = _GetAutoScrollSpeed ( cursorBelowBottomDist ) ;
}
else if ( cursorAboveTopDist > MinAutoScrollDist )
{
newAutoScrollVelocity = - 1.0 * _GetAutoScrollSpeed ( cursorAboveTopDist ) ;
}
if ( newAutoScrollVelocity ! = 0 )
{
_TryStartAutoScroll ( point , newAutoScrollVelocity ) ;
}
else
{
_TryStopAutoScroll ( ptr . PointerId ( ) ) ;
}
}
}
else if ( ptr . PointerDeviceType ( ) = = Windows : : Devices : : Input : : PointerDeviceType : : Touch & & _touchAnchor )
{
const auto contactRect = point . Properties ( ) . ContactRect ( ) ;
winrt : : Windows : : Foundation : : Point newTouchPoint { contactRect . X , contactRect . Y } ;
const auto anchor = _touchAnchor . value ( ) ;
// Get the difference between the point we've dragged to and the start of the touch.
const float fontHeight = float ( _actualFont . GetSize ( ) . Y ) ;
const float dy = newTouchPoint . Y - anchor . Y ;
2020-02-19 02:07:05 +01:00
// Start viewport scroll after we've moved more than a half row of text
if ( std : : abs ( dy ) > ( fontHeight / 2.0f ) )
2020-02-12 20:06:46 +01:00
{
// Multiply by -1, because moving the touch point down will
// create a positive delta, but we want the viewport to move up,
// so we'll need a negative scroll amount (and the inverse for
// panning down)
const float numRows = - 1.0f * ( dy / fontHeight ) ;
2020-02-19 02:07:05 +01:00
const auto currentOffset = : : base : : ClampedNumeric < double > ( _scrollBar . Value ( ) ) ;
const auto newValue = numRows + currentOffset ;
2020-02-12 20:06:46 +01:00
2020-02-19 02:07:05 +01:00
_scrollBar . Value ( newValue ) ;
2020-02-12 20:06:46 +01:00
// Use this point as our new scroll anchor.
_touchAnchor = newTouchPoint ;
}
}
args . Handled ( true ) ;
}
// Method Description:
// - Event handler for the PointerReleased event. We use this to de-anchor
// touch events, to stop scrolling via touch.
// Arguments:
// - sender: the XAML element responding to the pointer input
// - args: event data
void TermControl : : _PointerReleasedHandler ( Windows : : Foundation : : IInspectable const & sender ,
Input : : PointerRoutedEventArgs const & args )
{
_ReleasePointerCapture ( sender , args ) ;
const auto ptr = args . Pointer ( ) ;
if ( ptr . PointerDeviceType ( ) = = Windows : : Devices : : Input : : PointerDeviceType : : Mouse | | ptr . PointerDeviceType ( ) = = Windows : : Devices : : Input : : PointerDeviceType : : Pen )
{
const auto modifiers = static_cast < uint32_t > ( args . KeyModifiers ( ) ) ;
// static_cast to a uint32_t because we can't use the WI_IsFlagSet
// macro directly with a VirtualKeyModifiers
const auto shiftEnabled = WI_IsFlagSet ( modifiers , static_cast < uint32_t > ( VirtualKeyModifiers : : Shift ) ) ;
if ( _terminal - > IsCopyOnSelectActive ( ) )
{
2020-02-20 19:30:10 +01:00
// If the terminal was in focus, copy to clipboard.
// If the terminal was unfocused AND a click-drag selection happened, copy to clipboard.
if ( ! _unfocusedClickPos | | ( _unfocusedClickPos & & _isClickDragSelection ) )
{
CopySelectionToClipboard ( ! shiftEnabled ) ;
}
2020-02-12 20:06:46 +01:00
}
}
else if ( ptr . PointerDeviceType ( ) = = Windows : : Devices : : Input : : PointerDeviceType : : Touch )
{
_touchAnchor = std : : nullopt ;
}
2020-02-20 19:30:10 +01:00
_unfocusedClickPos = std : : nullopt ;
_isClickDragSelection = false ;
2020-02-12 20:06:46 +01:00
_TryStopAutoScroll ( ptr . PointerId ( ) ) ;
args . Handled ( true ) ;
}
// Method Description:
// - Event handler for the PointerWheelChanged event. This is raised in
// response to mouse wheel changes. Depending upon what modifier keys are
// pressed, different actions will take place.
// Arguments:
// - args: the event args containing information about t`he mouse wheel event.
void TermControl : : _MouseWheelHandler ( Windows : : Foundation : : IInspectable const & /*sender*/ ,
Input : : PointerRoutedEventArgs const & args )
{
const auto point = args . GetCurrentPoint ( _root ) ;
const auto delta = point . Properties ( ) . MouseWheelDelta ( ) ;
// Get the state of the Ctrl & Shift keys
// static_cast to a uint32_t because we can't use the WI_IsFlagSet macro
// directly with a VirtualKeyModifiers
const auto modifiers = static_cast < uint32_t > ( args . KeyModifiers ( ) ) ;
const auto ctrlPressed = WI_IsFlagSet ( modifiers , static_cast < uint32_t > ( VirtualKeyModifiers : : Control ) ) ;
const auto shiftPressed = WI_IsFlagSet ( modifiers , static_cast < uint32_t > ( VirtualKeyModifiers : : Shift ) ) ;
if ( ctrlPressed & & shiftPressed )
{
_MouseTransparencyHandler ( delta ) ;
}
else if ( ctrlPressed )
{
_MouseZoomHandler ( delta ) ;
}
else
{
_MouseScrollHandler ( delta , point ) ;
}
}
// Method Description:
// - Adjust the opacity of the acrylic background in response to a mouse
// scrolling event.
// Arguments:
// - mouseDelta: the mouse wheel delta that triggered this event.
void TermControl : : _MouseTransparencyHandler ( const double mouseDelta )
{
// Transparency is on a scale of [0.0,1.0], so only increment by .01.
const auto effectiveDelta = mouseDelta < 0 ? - .01 : .01 ;
if ( _settings . UseAcrylic ( ) )
{
try
{
auto acrylicBrush = _root . Background ( ) . as < Media : : AcrylicBrush > ( ) ;
acrylicBrush . TintOpacity ( acrylicBrush . TintOpacity ( ) + effectiveDelta ) ;
}
CATCH_LOG ( ) ;
}
}
// Method Description:
// - Adjust the font size of the terminal in response to a mouse scrolling
// event.
// Arguments:
// - mouseDelta: the mouse wheel delta that triggered this event.
void TermControl : : _MouseZoomHandler ( const double mouseDelta )
{
const auto fontDelta = mouseDelta < 0 ? - 1 : 1 ;
AdjustFontSize ( fontDelta ) ;
}
// Method Description:
// - Reset the font size of the terminal to its default size.
// Arguments:
// - none
void TermControl : : ResetFontSize ( )
{
_SetFontSize ( _settings . FontSize ( ) ) ;
}
// Method Description:
// - Adjust the font size of the terminal control.
// Arguments:
// - fontSizeDelta: The amount to increase or decrease the font size by.
void TermControl : : AdjustFontSize ( int fontSizeDelta )
{
const auto newSize = _desiredFont . GetEngineSize ( ) . Y + fontSizeDelta ;
_SetFontSize ( newSize ) ;
}
// Method Description:
// - Scroll the visible viewport in response to a mouse wheel event.
// Arguments:
// - mouseDelta: the mouse wheel delta that triggered this event.
void TermControl : : _MouseScrollHandler ( const double mouseDelta , Windows : : UI : : Input : : PointerPoint const & pointerPoint )
{
2020-02-19 02:07:05 +01:00
const auto currentOffset = _scrollBar . Value ( ) ;
2020-02-12 20:06:46 +01:00
// negative = down, positive = up
// However, for us, the signs are flipped.
2020-02-19 02:07:05 +01:00
const auto rowDelta = mouseDelta / ( - 1.0 * WHEEL_DELTA ) ;
2020-02-12 20:06:46 +01:00
// With one of the precision mouses, one click is always a multiple of 120,
// but the "smooth scrolling" mode results in non-int values
double newValue = ( _rowsToScroll * rowDelta ) + ( currentOffset ) ;
// The scroll bar's ValueChanged handler will actually move the viewport
// for us.
2020-02-19 02:07:05 +01:00
_scrollBar . Value ( newValue ) ;
2020-02-12 20:06:46 +01:00
if ( _terminal - > IsSelectionActive ( ) & & pointerPoint . Properties ( ) . IsLeftButtonPressed ( ) )
{
// If user is mouse selecting and scrolls, they then point at new character.
// Make sure selection reflects that immediately.
_SetEndSelectionPointAtCursor ( pointerPoint . Position ( ) ) ;
}
}
void TermControl : : _ScrollbarChangeHandler ( Windows : : Foundation : : IInspectable const & /*sender*/ ,
Controls : : Primitives : : RangeBaseValueChangedEventArgs const & args )
{
if ( _isTerminalInitiatedScroll )
{
return ;
}
const auto newValue = static_cast < int > ( args . NewValue ( ) ) ;
// This is a scroll event that wasn't initiated by the terminal
// itself - it was initiated by the mouse wheel, or the scrollbar.
_terminal - > UserScrollViewport ( newValue ) ;
// We've just told the terminal to update its viewport to reflect the
// new scroll value so the scroll bar matches the viewport now.
_willUpdateScrollBarToMatchViewport . store ( false ) ;
}
// Method Description:
// - captures the pointer so that none of the other XAML elements respond to pointer events
// Arguments:
// - sender: XAML element that is interacting with pointer
// - args: pointer data (i.e.: mouse, touch)
// Return Value:
// - true if we successfully capture the pointer, false otherwise.
bool TermControl : : _CapturePointer ( Windows : : Foundation : : IInspectable const & sender , Windows : : UI : : Xaml : : Input : : PointerRoutedEventArgs const & args )
{
IUIElement uielem ;
if ( sender . try_as ( uielem ) )
{
uielem . CapturePointer ( args . Pointer ( ) ) ;
return true ;
}
return false ;
}
// Method Description:
// - releases the captured pointer because we're done responding to XAML pointer events
// Arguments:
// - sender: XAML element that is interacting with pointer
// - args: pointer data (i.e.: mouse, touch)
// Return Value:
// - true if we release capture of the pointer, false otherwise.
bool TermControl : : _ReleasePointerCapture ( Windows : : Foundation : : IInspectable const & sender , Windows : : UI : : Xaml : : Input : : PointerRoutedEventArgs const & args )
{
IUIElement uielem ;
if ( sender . try_as ( uielem ) )
{
uielem . ReleasePointerCapture ( args . Pointer ( ) ) ;
return true ;
}
return false ;
}
// Method Description:
// - Starts new pointer related auto scroll behavior, or continues existing one.
// Does nothing when there is already auto scroll associated with another pointer.
// Arguments:
// - pointerPoint: info about pointer that causes auto scroll. Pointer's position
// is later used to update selection.
// - scrollVelocity: target velocity of scrolling in characters / sec
void TermControl : : _TryStartAutoScroll ( Windows : : UI : : Input : : PointerPoint const & pointerPoint , const double scrollVelocity )
{
// Allow only one pointer at the time
if ( ! _autoScrollingPointerPoint . has_value ( ) | | _autoScrollingPointerPoint . value ( ) . PointerId ( ) = = pointerPoint . PointerId ( ) )
{
_autoScrollingPointerPoint = pointerPoint ;
_autoScrollVelocity = scrollVelocity ;
// If this is first time the auto scroll update is about to be called,
// kick-start it by initializing its time delta as if it started now
if ( ! _lastAutoScrollUpdateTime . has_value ( ) )
{
_lastAutoScrollUpdateTime = std : : chrono : : high_resolution_clock : : now ( ) ;
}
// Apparently this check is not necessary but greatly improves performance
if ( ! _autoScrollTimer . IsEnabled ( ) )
{
_autoScrollTimer . Start ( ) ;
}
}
}
// Method Description:
// - Stops auto scroll if it's active and is associated with supplied pointer id.
// Arguments:
// - pointerId: id of pointer for which to stop auto scroll
void TermControl : : _TryStopAutoScroll ( const uint32_t pointerId )
{
if ( _autoScrollingPointerPoint . has_value ( ) & & pointerId = = _autoScrollingPointerPoint . value ( ) . PointerId ( ) )
{
_autoScrollingPointerPoint = std : : nullopt ;
_autoScrollVelocity = 0 ;
_lastAutoScrollUpdateTime = std : : nullopt ;
// Apparently this check is not necessary but greatly improves performance
if ( _autoScrollTimer . IsEnabled ( ) )
{
_autoScrollTimer . Stop ( ) ;
}
}
}
// Method Description:
// - Called continuously to gradually scroll viewport when user is
// mouse selecting outside it (to 'follow' the cursor).
// Arguments:
// - none
void TermControl : : _UpdateAutoScroll ( Windows : : Foundation : : IInspectable const & /* sender */ ,
Windows : : Foundation : : IInspectable const & /* e */ )
{
if ( _autoScrollVelocity ! = 0 )
{
const auto timeNow = std : : chrono : : high_resolution_clock : : now ( ) ;
if ( _lastAutoScrollUpdateTime . has_value ( ) )
{
static constexpr double microSecPerSec = 1000000.0 ;
const double deltaTime = std : : chrono : : duration_cast < std : : chrono : : microseconds > ( timeNow - _lastAutoScrollUpdateTime . value ( ) ) . count ( ) / microSecPerSec ;
_scrollBar . Value ( _scrollBar . Value ( ) + _autoScrollVelocity * deltaTime ) ;
if ( _autoScrollingPointerPoint . has_value ( ) )
{
_SetEndSelectionPointAtCursor ( _autoScrollingPointerPoint . value ( ) . Position ( ) ) ;
}
}
_lastAutoScrollUpdateTime = timeNow ;
}
}
// Method Description:
// - Event handler for the GotFocus event. This is used to...
// - enable accessibility notifications for this TermControl
// - start blinking the cursor when the window is focused
// - update the number of lines to scroll to the value set in the system
void TermControl : : _GotFocusHandler ( Windows : : Foundation : : IInspectable const & /* sender */ ,
RoutedEventArgs const & /* args */ )
{
if ( _closing )
{
return ;
}
_focused = true ;
if ( _uiaEngine . get ( ) )
{
THROW_IF_FAILED ( _uiaEngine - > Enable ( ) ) ;
}
if ( _tsfInputControl ! = nullptr )
{
_tsfInputControl . NotifyFocusEnter ( ) ;
}
if ( _cursorTimer . has_value ( ) )
{
// When the terminal focuses, show the cursor immediately
_terminal - > SetCursorVisible ( true ) ;
_cursorTimer . value ( ) . Start ( ) ;
}
_rowsToScroll = _settings . RowsToScroll ( ) ;
}
// Method Description:
// - Event handler for the LostFocus event. This is used to...
// - disable accessibility notifications for this TermControl
// - hide and stop blinking the cursor when the window loses focus.
void TermControl : : _LostFocusHandler ( Windows : : Foundation : : IInspectable const & /* sender */ ,
RoutedEventArgs const & /* args */ )
{
if ( _closing )
{
return ;
}
_focused = false ;
if ( _uiaEngine . get ( ) )
{
THROW_IF_FAILED ( _uiaEngine - > Disable ( ) ) ;
}
if ( _tsfInputControl ! = nullptr )
{
_tsfInputControl . NotifyFocusLeave ( ) ;
}
if ( _cursorTimer . has_value ( ) )
{
_cursorTimer . value ( ) . Stop ( ) ;
_terminal - > SetCursorVisible ( false ) ;
}
}
// Method Description:
// - Writes the given sequence as input to the active terminal connection,
// Arguments:
// - wstr: the string of characters to write to the terminal connection.
// Return Value:
// - <none>
void TermControl : : _SendInputToConnection ( const std : : wstring & wstr )
{
_connection . WriteInput ( wstr ) ;
}
// Method Description:
// - Pre-process text pasted (presumably from the clipboard)
// before sending it over the terminal's connection, converting
// Windows-space \r\n line-endings to \r line-endings
void TermControl : : _SendPastedTextToConnection ( const std : : wstring & wstr )
{
// Some notes on this implementation:
//
// - std::regex can do this in a single line, but is somewhat
// overkill for a simple search/replace operation (and its
// performance guarantees aren't exactly stellar)
// - The STL doesn't have a simple string search/replace method.
// This fact is lamentable.
// - This line-ending conversion is intentionally fairly
// conservative, to avoid stripping out lone \n characters
// where they could conceivably be intentional.
std : : wstring stripped { wstr } ;
std : : wstring : : size_type pos = 0 ;
while ( ( pos = stripped . find ( L " \r \n " , pos ) ) ! = std : : wstring : : npos )
{
stripped . replace ( pos , 2 , L " \r " ) ;
}
_connection . WriteInput ( stripped ) ;
_terminal - > TrySnapOnInput ( ) ;
}
// Method Description:
// - Update the font with the renderer. This will be called either when the
// font changes or the DPI changes, as DPI changes will necessitate a
// font change. This method will *not* change the buffer/viewport size
// to account for the new glyph dimensions. Callers should make sure to
// appropriately call _DoResize after this method is called.
// Arguments:
// - initialUpdate: whether this font update should be considered as being
// concerned with initialization process. Value forwarded to event handler.
void TermControl : : _UpdateFont ( const bool initialUpdate )
{
auto lock = _terminal - > LockForWriting ( ) ;
const int newDpi = static_cast < int > ( static_cast < double > ( USER_DEFAULT_SCREEN_DPI ) * _swapChainPanel . CompositionScaleX ( ) ) ;
// TODO: MSFT:20895307 If the font doesn't exist, this doesn't
// actually fail. We need a way to gracefully fallback.
_renderer - > TriggerFontChange ( newDpi , _desiredFont , _actualFont ) ;
const auto actualNewSize = _actualFont . GetSize ( ) ;
_fontSizeChangedHandlers ( actualNewSize . X , actualNewSize . Y , initialUpdate ) ;
}
// Method Description:
// - Set the font size of the terminal control.
// Arguments:
// - fontSize: The size of the font.
void TermControl : : _SetFontSize ( int fontSize )
{
try
{
// Make sure we have a non-zero font size
const auto newSize = std : : max ( gsl : : narrow < short > ( fontSize ) , static_cast < short > ( 1 ) ) ;
const auto fontFace = _settings . FontFace ( ) ;
_actualFont = { fontFace , 0 , 10 , { 0 , newSize } , CP_UTF8 , false } ;
_desiredFont = { _actualFont } ;
// Refresh our font with the renderer
_UpdateFont ( ) ;
// Resize the terminal's BUFFER to match the new font size. This does
// NOT change the size of the window, because that can lead to more
// problems (like what happens when you change the font size while the
// window is maximized?)
auto lock = _terminal - > LockForWriting ( ) ;
_DoResize ( _swapChainPanel . ActualWidth ( ) , _swapChainPanel . ActualHeight ( ) ) ;
}
CATCH_LOG ( ) ;
}
// Method Description:
// - Triggered when the swapchain changes size. We use this to resize the
// terminal buffers to match the new visible size.
// Arguments:
// - e: a SizeChangedEventArgs with the new dimensions of the SwapChainPanel
void TermControl : : _SwapChainSizeChanged ( winrt : : Windows : : Foundation : : IInspectable const & /*sender*/ ,
SizeChangedEventArgs const & e )
{
if ( ! _initializedTerminal )
{
return ;
}
auto lock = _terminal - > LockForWriting ( ) ;
const auto foundationSize = e . NewSize ( ) ;
_DoResize ( foundationSize . Width , foundationSize . Height ) ;
}
void TermControl : : _SwapChainScaleChanged ( Windows : : UI : : Xaml : : Controls : : SwapChainPanel const & sender ,
Windows : : Foundation : : IInspectable const & /*args*/ )
{
2020-02-13 01:42:06 +01:00
if ( _renderEngine )
{
const auto scale = sender . CompositionScaleX ( ) ;
const auto dpi = ( int ) ( scale * USER_DEFAULT_SCREEN_DPI ) ;
2020-02-12 20:06:46 +01:00
2020-02-13 01:42:06 +01:00
// TODO: MSFT: 21169071 - Shouldn't this all happen through _renderer and trigger the invalidate automatically on DPI change?
THROW_IF_FAILED ( _renderEngine - > UpdateDpi ( dpi ) ) ;
_renderer - > TriggerRedrawAll ( ) ;
}
2020-02-12 20:06:46 +01:00
}
// Method Description:
// - Toggle the cursor on and off when called by the cursor blink timer.
// Arguments:
// - sender: not used
// - e: not used
void TermControl : : _BlinkCursor ( Windows : : Foundation : : IInspectable const & /* sender */ ,
Windows : : Foundation : : IInspectable const & /* e */ )
{
if ( ( _closing ) | | ( ! _terminal - > IsCursorBlinkingAllowed ( ) & & _terminal - > IsCursorVisible ( ) ) )
{
return ;
}
_terminal - > SetCursorVisible ( ! _terminal - > IsCursorVisible ( ) ) ;
}
// Method Description:
// - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging.
// Arguments:
// - cursorPosition: in pixels, relative to the origin of the control
void TermControl : : _SetEndSelectionPointAtCursor ( Windows : : Foundation : : Point const & cursorPosition )
{
auto terminalPosition = _GetTerminalPosition ( cursorPosition ) ;
const short lastVisibleRow = std : : max < short > ( _terminal - > GetViewport ( ) . Height ( ) - 1 , 0 ) ;
const short lastVisibleCol = std : : max < short > ( _terminal - > GetViewport ( ) . Width ( ) - 1 , 0 ) ;
terminalPosition . Y = std : : clamp < short > ( terminalPosition . Y , 0 , lastVisibleRow ) ;
terminalPosition . X = std : : clamp < short > ( terminalPosition . X , 0 , lastVisibleCol ) ;
// save location (for rendering) + render
_terminal - > SetEndSelectionPosition ( terminalPosition ) ;
_renderer - > TriggerSelection ( ) ;
}
// Method Description:
// - Process a resize event that was initiated by the user. This can either
// be due to the user resizing the window (causing the swapchain to
// resize) or due to the DPI changing (causing us to need to resize the
// buffer to match)
// Arguments:
// - newWidth: the new width of the swapchain, in pixels.
// - newHeight: the new height of the swapchain, in pixels.
void TermControl : : _DoResize ( const double newWidth , const double newHeight )
{
SIZE size ;
size . cx = static_cast < long > ( newWidth ) ;
size . cy = static_cast < long > ( newHeight ) ;
// Don't actually resize so small that a single character wouldn't fit
// in either dimension. The buffer really doesn't like being size 0.
if ( size . cx < _actualFont . GetSize ( ) . X | | size . cy < _actualFont . GetSize ( ) . Y )
{
return ;
}
// Tell the dx engine that our window is now the new size.
THROW_IF_FAILED ( _renderEngine - > SetWindowSize ( size ) ) ;
// Invalidate everything
_renderer - > TriggerRedrawAll ( ) ;
// Convert our new dimensions to characters
const auto viewInPixels = Viewport : : FromDimensions ( { 0 , 0 } ,
{ static_cast < short > ( size . cx ) , static_cast < short > ( size . cy ) } ) ;
const auto vp = _renderEngine - > GetViewportInCharacters ( viewInPixels ) ;
// If this function succeeds with S_FALSE, then the terminal didn't
// actually change size. No need to notify the connection of this
// no-op.
// TODO: MSFT:20642295 Resizing the buffer will corrupt it
// I believe we'll need support for CSI 2J, and additionally I think
// we're resetting the viewport to the top
const HRESULT hr = _terminal - > UserResize ( { vp . Width ( ) , vp . Height ( ) } ) ;
if ( SUCCEEDED ( hr ) & & hr ! = S_FALSE )
{
_connection . Resize ( vp . Height ( ) , vp . Width ( ) ) ;
}
}
void TermControl : : _TerminalTitleChanged ( const std : : wstring_view & wstr )
{
_titleChangedHandlers ( winrt : : hstring { wstr } ) ;
}
// Method Description:
// - Update the position and size of the scrollbar to match the given
// viewport top, viewport height, and buffer size.
// The change will be actually handled in _ScrollbarChangeHandler.
// This should be done on the UI thread. Make sure the caller is calling
// us in a RunAsync block.
// Arguments:
// - viewTop: the top of the visible viewport, in rows. 0 indicates the top
// of the buffer.
// - viewHeight: the height of the viewport in rows.
// - bufferSize: the length of the buffer, in rows
void TermControl : : _ScrollbarUpdater ( Controls : : Primitives : : ScrollBar scrollBar ,
const int viewTop ,
const int viewHeight ,
const int bufferSize )
{
// The terminal is already in the scroll position it wants, so no need
// to tell it to scroll.
_isTerminalInitiatedScroll = true ;
const auto hiddenContent = bufferSize - viewHeight ;
scrollBar . Maximum ( hiddenContent ) ;
scrollBar . Minimum ( 0 ) ;
scrollBar . ViewportSize ( viewHeight ) ;
scrollBar . Value ( viewTop ) ;
_isTerminalInitiatedScroll = false ;
}
// Method Description:
// - Update the position and size of the scrollbar to match the given
// viewport top, viewport height, and buffer size.
// Additionally fires a ScrollPositionChanged event for anyone who's
// registered an event handler for us.
// Arguments:
// - viewTop: the top of the visible viewport, in rows. 0 indicates the top
// of the buffer.
// - viewHeight: the height of the viewport in rows.
// - bufferSize: the length of the buffer, in rows
winrt : : fire_and_forget TermControl : : _TerminalScrollPositionChanged ( const int viewTop ,
const int viewHeight ,
const int bufferSize )
{
// Since this callback fires from non-UI thread, we might be already
// closed/closing.
if ( _closing . load ( ) )
{
return ;
}
_scrollPositionChangedHandlers ( viewTop , viewHeight , bufferSize ) ;
auto weakThis { get_weak ( ) } ;
co_await winrt : : resume_foreground ( _scrollBar . Dispatcher ( ) ) ;
// Even if we weren't closed/closing few lines above, we might be
// while waiting for this block of code to be dispatched.
// If 'weakThis' is locked, then we can safely work with 'this'
if ( auto control { weakThis . get ( ) } )
{
if ( ! _closing . load ( ) )
{
// Update our scrollbar
_ScrollbarUpdater ( _scrollBar , viewTop , viewHeight , bufferSize ) ;
}
}
}
hstring TermControl : : Title ( )
{
if ( ! _initializedTerminal )
return L " " ;
hstring hstr ( _terminal - > GetConsoleTitle ( ) ) ;
return hstr ;
}
// Method Description:
// - Given a copy-able selection, get the selected text from the buffer and send it to the
// Windows Clipboard (CascadiaWin32:main.cpp).
// - CopyOnSelect does NOT clear the selection
// Arguments:
// - trimTrailingWhitespace: enable removing any whitespace from copied selection
// and get text to appear on separate lines.
bool TermControl : : CopySelectionToClipboard ( bool trimTrailingWhitespace )
{
// no selection --> nothing to copy
if ( _terminal = = nullptr | | ! _terminal - > IsSelectionActive ( ) )
{
return false ;
}
// extract text from buffer
const auto bufferData = _terminal - > RetrieveSelectedTextFromBuffer ( trimTrailingWhitespace ) ;
// convert text: vector<string> --> string
std : : wstring textData ;
for ( const auto & text : bufferData . text )
{
textData + = text ;
}
// convert text to HTML format
const auto htmlData = TextBuffer : : GenHTML ( bufferData ,
_actualFont . GetUnscaledSize ( ) . Y ,
_actualFont . GetFaceName ( ) ,
_settings . DefaultBackground ( ) ,
" Windows Terminal " ) ;
// convert to RTF format
const auto rtfData = TextBuffer : : GenRTF ( bufferData ,
_actualFont . GetUnscaledSize ( ) . Y ,
_actualFont . GetFaceName ( ) ,
_settings . DefaultBackground ( ) ) ;
if ( ! _terminal - > IsCopyOnSelectActive ( ) )
{
_terminal - > ClearSelection ( ) ;
}
// send data up for clipboard
auto copyArgs = winrt : : make_self < CopyToClipboardEventArgs > ( winrt : : hstring ( textData ) ,
winrt : : to_hstring ( htmlData ) ,
winrt : : to_hstring ( rtfData ) ) ;
_clipboardCopyHandlers ( * this , * copyArgs ) ;
return true ;
}
// Method Description:
// - Initiate a paste operation.
void TermControl : : PasteTextFromClipboard ( )
{
// attach TermControl::_SendInputToConnection() as the clipboardDataHandler.
// This is called when the clipboard data is loaded.
auto clipboardDataHandler = std : : bind ( & TermControl : : _SendPastedTextToConnection , this , std : : placeholders : : _1 ) ;
auto pasteArgs = winrt : : make_self < PasteFromClipboardEventArgs > ( clipboardDataHandler ) ;
// send paste event up to TermApp
_clipboardPasteHandlers ( * this , * pasteArgs ) ;
}
void TermControl : : Close ( )
{
if ( ! _closing . exchange ( true ) )
{
// Stop accepting new output and state changes before we disconnect everything.
_connection . TerminalOutput ( _connectionOutputEventToken ) ;
_connectionStateChangedRevoker . revoke ( ) ;
_tsfInputControl . Close ( ) ; // Disconnect the TSF input control so it doesn't receive EditContext events.
if ( auto localConnection { std : : exchange ( _connection , nullptr ) } )
{
localConnection . Close ( ) ;
// connection is destroyed.
}
if ( auto localRenderEngine { std : : exchange ( _renderEngine , nullptr ) } )
{
if ( auto localRenderer { std : : exchange ( _renderer , nullptr ) } )
{
localRenderer - > TriggerTeardown ( ) ;
// renderer is destroyed
}
// renderEngine is destroyed
}
if ( auto localTerminal { std : : exchange ( _terminal , nullptr ) } )
{
_initializedTerminal = false ;
// terminal is destroyed.
}
}
}
// Method Description:
// - Scrolls the viewport of the terminal and updates the scroll bar accordingly
// Arguments:
// - viewTop: the viewTop to scroll to
void TermControl : : ScrollViewport ( int viewTop )
{
_scrollBar . Value ( viewTop ) ;
}
int TermControl : : GetScrollOffset ( )
{
return _terminal - > GetScrollOffset ( ) ;
}
// Function Description:
// - Gets the height of the terminal in lines of text
// Return Value:
// - The height of the terminal in lines of text
int TermControl : : GetViewHeight ( ) const
{
const auto viewPort = _terminal - > GetViewport ( ) ;
return viewPort . Height ( ) ;
}
// Function Description:
// - Determines how much space (in pixels) an app would need to reserve to
// create a control with the settings stored in the settings param. This
// accounts for things like the font size and face, the initialRows and
// initialCols, and scrollbar visibility. The returned sized is based upon
// the provided DPI value
// Arguments:
// - settings: A IControlSettings with the settings to get the pixel size of.
// - dpi: The DPI we should create the terminal at. This affects things such
// as font size, scrollbar and other control scaling, etc. Make sure the
// caller knows what monitor the control is about to appear on.
// Return Value:
// - a point containing the requested dimensions in pixels.
winrt : : Windows : : Foundation : : Point TermControl : : GetProposedDimensions ( IControlSettings const & settings , const uint32_t dpi )
{
// Initialize our font information.
const auto fontFace = settings . FontFace ( ) ;
const short fontHeight = gsl : : narrow < short > ( settings . FontSize ( ) ) ;
// The font width doesn't terribly matter, we'll only be using the
// height to look it up
// The other params here also largely don't matter.
// The family is only used to determine if the font is truetype or
// not, but DX doesn't use that info at all.
// The Codepage is additionally not actually used by the DX engine at all.
FontInfo actualFont = { fontFace , 0 , 10 , { 0 , fontHeight } , CP_UTF8 , false } ;
FontInfoDesired desiredFont = { actualFont } ;
// If the settings have negative or zero row or column counts, ignore those counts.
// (The lower TerminalCore layer also has upper bounds as well, but at this layer
// we may eventually impose different ones depending on how many pixels we can address.)
const auto cols = std : : max ( settings . InitialCols ( ) , 1 ) ;
const auto rows = std : : max ( settings . InitialRows ( ) , 1 ) ;
// Create a DX engine and initialize it with our font and DPI. We'll
// then use it to measure how much space the requested rows and columns
// will take up.
// TODO: MSFT:21254947 - use a static function to do this instead of
// instantiating a DxEngine
auto dxEngine = std : : make_unique < : : Microsoft : : Console : : Render : : DxEngine > ( ) ;
THROW_IF_FAILED ( dxEngine - > UpdateDpi ( dpi ) ) ;
THROW_IF_FAILED ( dxEngine - > UpdateFont ( desiredFont , actualFont ) ) ;
const float scale = dxEngine - > GetScaling ( ) ;
const auto fontSize = actualFont . GetSize ( ) ;
// Manually multiply by the scaling factor. The DX engine doesn't
// actually store the scaled font size in the fontInfo.GetSize()
// property when the DX engine is in Composition mode (which it is for
// the Terminal). At runtime, this is fine, as we'll transform
// everything by our scaling, so it'll work out. However, right now we
// need to get the exact pixel count.
const float fFontWidth = gsl : : narrow < float > ( fontSize . X * scale ) ;
const float fFontHeight = gsl : : narrow < float > ( fontSize . Y * scale ) ;
// UWP XAML scrollbars aren't guaranteed to be the same size as the
// ComCtl scrollbars, but it's certainly close enough.
const auto scrollbarSize = GetSystemMetricsForDpi ( SM_CXVSCROLL , dpi ) ;
double width = cols * fFontWidth ;
// Reserve additional space if scrollbar is intended to be visible
if ( settings . ScrollState ( ) = = ScrollbarState : : Visible )
{
width + = scrollbarSize ;
}
double height = rows * fFontHeight ;
auto thickness = _ParseThicknessFromPadding ( settings . Padding ( ) ) ;
width + = thickness . Left + thickness . Right ;
height + = thickness . Top + thickness . Bottom ;
return { gsl : : narrow_cast < float > ( width ) , gsl : : narrow_cast < float > ( height ) } ;
}
// Method Description:
// - Get the size of a single character of this control. The size is in
// DIPs. If you need it in _pixels_, you'll need to multiply by the
// current display scaling.
// Arguments:
// - <none>
// Return Value:
// - The dimensions of a single character of this control, in DIPs
winrt : : Windows : : Foundation : : Size TermControl : : CharacterDimensions ( ) const
{
const auto fontSize = _actualFont . GetSize ( ) ;
return { gsl : : narrow_cast < float > ( fontSize . X ) , gsl : : narrow_cast < float > ( fontSize . Y ) } ;
}
// Method Description:
// - Get the absolute minimum size that this control can be resized to and
// still have 1x1 character visible. This includes the space needed for
// the scrollbar and the padding.
// Arguments:
// - <none>
// Return Value:
// - The minimum size that this terminal control can be resized to and still
// have a visible character.
winrt : : Windows : : Foundation : : Size TermControl : : MinimumSize ( ) const
{
const auto fontSize = _actualFont . GetSize ( ) ;
double width = fontSize . X ;
double height = fontSize . Y ;
// Reserve additional space if scrollbar is intended to be visible
if ( _settings . ScrollState ( ) = = ScrollbarState : : Visible )
{
width + = _scrollBar . ActualWidth ( ) ;
}
// Account for the size of any padding
const auto padding = _swapChainPanel . Margin ( ) ;
width + = padding . Left + padding . Right ;
height + = padding . Top + padding . Bottom ;
return { gsl : : narrow_cast < float > ( width ) , gsl : : narrow_cast < float > ( height ) } ;
}
// Method Description:
// - Adjusts given dimension (width or height) so that it aligns to the character grid.
// The snap is always downward.
// Arguments:
// - widthOrHeight: if true operates on width, otherwise on height
// - dimension: a dimension (width or height) to be snapped
// Return Value:
// - A dimension that would be aligned to the character grid.
float TermControl : : SnapDimensionToGrid ( const bool widthOrHeight , const float dimension ) const
{
const auto fontSize = _actualFont . GetSize ( ) ;
const auto fontDimension = widthOrHeight ? fontSize . X : fontSize . Y ;
const auto padding = _swapChainPanel . Margin ( ) ;
auto nonTerminalArea = gsl : : narrow_cast < float > ( widthOrHeight ?
padding . Left + padding . Right :
padding . Top + padding . Bottom ) ;
if ( widthOrHeight & & _settings . ScrollState ( ) = = ScrollbarState : : Visible )
{
nonTerminalArea + = gsl : : narrow_cast < float > ( _scrollBar . ActualWidth ( ) ) ;
}
const auto gridSize = dimension - nonTerminalArea ;
const int cells = static_cast < int > ( gridSize / fontDimension ) ;
return cells * fontDimension + nonTerminalArea ;
}
// Method Description:
// - Create XAML Thickness object based on padding props provided.
// Used for controlling the TermControl XAML Grid container's Padding prop.
// Arguments:
// - padding: 2D padding values
// Single Double value provides uniform padding
// Two Double values provide isometric horizontal & vertical padding
// Four Double values provide independent padding for 4 sides of the bounding rectangle
// Return Value:
// - Windows::UI::Xaml::Thickness object
Windows : : UI : : Xaml : : Thickness TermControl : : _ParseThicknessFromPadding ( const hstring padding )
{
const wchar_t singleCharDelim = L ' , ' ;
std : : wstringstream tokenStream ( padding . c_str ( ) ) ;
std : : wstring token ;
uint8_t paddingPropIndex = 0 ;
std : : array < double , 4 > thicknessArr = { } ;
size_t * idx = nullptr ;
// Get padding values till we run out of delimiter separated values in the stream
// or we hit max number of allowable values (= 4) for the bounding rectangle
// Non-numeral values detected will default to 0
// std::getline will not throw exception unless flags are set on the wstringstream
// std::stod will throw invalid_argument exception if the input is an invalid double value
// std::stod will throw out_of_range exception if the input value is more than DBL_MAX
try
{
for ( ; std : : getline ( tokenStream , token , singleCharDelim ) & & ( paddingPropIndex < thicknessArr . size ( ) ) ; paddingPropIndex + + )
{
// std::stod internally calls wcstod which handles whitespace prefix (which is ignored)
// & stops the scan when first char outside the range of radix is encountered
// We'll be permissive till the extent that stod function allows us to be by default
// Ex. a value like 100.3#535w2 will be read as 100.3, but ;df25 will fail
thicknessArr [ paddingPropIndex ] = std : : stod ( token , idx ) ;
}
}
catch ( . . . )
{
// If something goes wrong, even if due to a single bad padding value, we'll reset the index & return default 0 padding
paddingPropIndex = 0 ;
LOG_CAUGHT_EXCEPTION ( ) ;
}
switch ( paddingPropIndex )
{
case 1 :
return ThicknessHelper : : FromUniformLength ( thicknessArr [ 0 ] ) ;
case 2 :
return ThicknessHelper : : FromLengths ( thicknessArr [ 0 ] , thicknessArr [ 1 ] , thicknessArr [ 0 ] , thicknessArr [ 1 ] ) ;
// No case for paddingPropIndex = 3, since it's not a norm to provide just Left, Top & Right padding values leaving out Bottom
case 4 :
return ThicknessHelper : : FromLengths ( thicknessArr [ 0 ] , thicknessArr [ 1 ] , thicknessArr [ 2 ] , thicknessArr [ 3 ] ) ;
default :
return Thickness ( ) ;
}
}
// Method Description:
// - Get the modifier keys that are currently pressed. This can be used to
// find out which modifiers (ctrl, alt, shift) are pressed in events that
// don't necessarily include that state.
// Return Value:
// - The Microsoft::Terminal::Core::ControlKeyStates representing the modifier key states.
ControlKeyStates TermControl : : _GetPressedModifierKeys ( ) const
{
CoreWindow window = CoreWindow : : GetForCurrentThread ( ) ;
// DONT USE
// != CoreVirtualKeyStates::None
// OR
// == CoreVirtualKeyStates::Down
// Sometimes with the key down, the state is Down | Locked.
// Sometimes with the key up, the state is Locked.
// IsFlagSet(Down) is the only correct solution.
struct KeyModifier
{
VirtualKey vkey ;
ControlKeyStates flags ;
} ;
constexpr std : : array < KeyModifier , 5 > modifiers { {
{ VirtualKey : : RightMenu , ControlKeyStates : : RightAltPressed } ,
{ VirtualKey : : LeftMenu , ControlKeyStates : : LeftAltPressed } ,
{ VirtualKey : : RightControl , ControlKeyStates : : RightCtrlPressed } ,
{ VirtualKey : : LeftControl , ControlKeyStates : : LeftCtrlPressed } ,
{ VirtualKey : : Shift , ControlKeyStates : : ShiftPressed } ,
} } ;
ControlKeyStates flags ;
for ( const auto & mod : modifiers )
{
const auto state = window . GetKeyState ( mod . vkey ) ;
const auto isDown = WI_IsFlagSet ( state , CoreVirtualKeyStates : : Down ) ;
if ( isDown )
{
flags | = mod . flags ;
}
}
return flags ;
}
// Method Description:
// - Gets the corresponding viewport terminal position for the cursor
// by excluding the padding and normalizing with the font size.
// This is used for selection.
// Arguments:
// - cursorPosition: the (x,y) position of a given cursor (i.e.: mouse cursor).
// NOTE: origin (0,0) is top-left.
// Return Value:
// - the corresponding viewport terminal position for the given Point parameter
const COORD TermControl : : _GetTerminalPosition ( winrt : : Windows : : Foundation : : Point cursorPosition )
{
// Exclude padding from cursor position calculation
COORD terminalPosition = {
static_cast < SHORT > ( cursorPosition . X - _swapChainPanel . Margin ( ) . Left ) ,
static_cast < SHORT > ( cursorPosition . Y - _swapChainPanel . Margin ( ) . Top )
} ;
const auto fontSize = _actualFont . GetSize ( ) ;
FAIL_FAST_IF ( fontSize . X = = 0 ) ;
FAIL_FAST_IF ( fontSize . Y = = 0 ) ;
// Normalize to terminal coordinates by using font size
terminalPosition . X / = fontSize . X ;
terminalPosition . Y / = fontSize . Y ;
return terminalPosition ;
}
// Method Description:
// - Composition Completion handler for the TSFInputControl that
// handles writing text out to TerminalConnection
// Arguments:
// - text: the text to write to TerminalConnection
// Return Value:
// - <none>
void TermControl : : _CompositionCompleted ( winrt : : hstring text )
{
_connection . WriteInput ( text ) ;
}
// Method Description:
// - CurrentCursorPosition handler for the TSFInputControl that
// handles returning current cursor position.
// Arguments:
// - eventArgs: event for storing the current cursor position
// Return Value:
// - <none>
void TermControl : : _CurrentCursorPositionHandler ( const IInspectable & /*sender*/ , const CursorPositionEventArgs & eventArgs )
{
const COORD cursorPos = _terminal - > GetCursorPosition ( ) ;
Windows : : Foundation : : Point p = { gsl : : narrow < float > ( cursorPos . X ) , gsl : : narrow < float > ( cursorPos . Y ) } ;
eventArgs . CurrentPosition ( p ) ;
}
// Method Description:
// - FontInfo handler for the TSFInputControl that
// handles returning current font information
// Arguments:
// - eventArgs: event for storing the current font information
// Return Value:
// - <none>
void TermControl : : _FontInfoHandler ( const IInspectable & /*sender*/ , const FontInfoEventArgs & eventArgs )
{
eventArgs . FontSize ( CharacterDimensions ( ) ) ;
eventArgs . FontFace ( _actualFont . GetFaceName ( ) ) ;
}
// Method Description:
// - Returns the number of clicks that occurred (double and triple click support)
// Arguments:
// - clickPos: the (x,y) position of a given cursor (i.e.: mouse cursor).
// NOTE: origin (0,0) is top-left.
// - clickTime: the timestamp that the click occurred
// Return Value:
// - if the click is in the same position as the last click and within the timeout, the number of clicks within that time window
// - otherwise, 1
const unsigned int TermControl : : _NumberOfClicks ( winrt : : Windows : : Foundation : : Point clickPos , Timestamp clickTime )
{
// if click occurred at a different location or past the multiClickTimer...
Timestamp delta ;
THROW_IF_FAILED ( UInt64Sub ( clickTime , _lastMouseClick , & delta ) ) ;
if ( clickPos ! = _lastMouseClickPos | | delta > _multiClickTimer )
{
// exit early. This is a single click.
_multiClickCounter = 1 ;
}
else
{
_multiClickCounter + + ;
}
return _multiClickCounter ;
}
// Method Description:
// - Calculates speed of single axis of auto scrolling. It has to allow for both
// fast and precise selection.
// Arguments:
// - cursorDistanceFromBorder: distance from viewport border to cursor, in pixels. Must be non-negative.
// Return Value:
// - positive speed in characters / sec
double TermControl : : _GetAutoScrollSpeed ( double cursorDistanceFromBorder ) const
{
// The numbers below just feel well, feel free to change.
// TODO: Maybe account for space beyond border that user has available
return std : : pow ( cursorDistanceFromBorder , 2.0 ) / 25.0 + 2.0 ;
}
// Method Description:
// - Async handler for the "Drop" event. If a file was dropped onto our
// root, we'll try to get the path of the file dropped onto us, and write
// the full path of the file to our terminal connection. Like conhost, if
// the path contains a space, we'll wrap the path in quotes.
// - Unlike conhost, if multiple files are dropped onto the terminal, we'll
// write all the paths to the terminal, separated by spaces.
// Arguments:
// - e: The DragEventArgs from the Drop event
// Return Value:
// - <none>
winrt : : fire_and_forget TermControl : : _DoDragDrop ( DragEventArgs const e )
{
if ( e . DataView ( ) . Contains ( StandardDataFormats : : StorageItems ( ) ) )
{
auto items = co_await e . DataView ( ) . GetStorageItemsAsync ( ) ;
if ( items . Size ( ) > 0 )
{
std : : wstring allPaths ;
for ( auto item : items )
{
// Join the paths with spaces
if ( ! allPaths . empty ( ) )
{
allPaths + = L " " ;
}
std : : wstring fullPath { item . Path ( ) } ;
const auto containsSpaces = std : : find ( fullPath . begin ( ) ,
fullPath . end ( ) ,
L ' ' ) ! = fullPath . end ( ) ;
auto lock = _terminal - > LockForWriting ( ) ;
if ( containsSpaces )
{
fullPath . insert ( 0 , L " \" " ) ;
fullPath + = L " \" " ;
}
allPaths + = fullPath ;
}
_SendInputToConnection ( allPaths ) ;
}
}
}
// Method Description:
// - Synchronous handler for the "Drop" event. We'll dispatch the async
// _DoDragDrop method to handle this, because getting information about
// the file that was potentially dropped onto us must be done off the UI
// thread.
// Arguments:
// - e: The DragEventArgs from the Drop event
// Return Value:
// - <none>
void TermControl : : _DragDropHandler ( Windows : : Foundation : : IInspectable const & /*sender*/ ,
DragEventArgs const & e )
{
// Dispatch an async method to handle the drop event.
_DoDragDrop ( e ) ;
}
// Method Description:
// - Handle the DragOver event. We'll signal that the drag operation we
// support is the "copy" operation, and we'll also customize the
// appearance of the drag-drop UI, by removing the preview and setting a
// custom caption. For more information, see
// https://docs.microsoft.com/en-us/windows/uwp/design/input/drag-and-drop#customize-the-ui
// Arguments:
// - e: The DragEventArgs from the DragOver event
// Return Value:
// - <none>
void TermControl : : _DragOverHandler ( Windows : : Foundation : : IInspectable const & /*sender*/ ,
DragEventArgs const & e )
{
if ( ! e . DataView ( ) . Contains ( StandardDataFormats : : StorageItems ( ) ) )
{
// We can't do anything for non-storageitems right now.
return ;
}
// Make sure to set the AcceptedOperation, so that we can later receive the path in the Drop event
e . AcceptedOperation ( DataPackageOperation : : Copy ) ;
// Sets custom UI text
e . DragUIOverride ( ) . Caption ( RS_ ( L " DragFileCaption " ) ) ;
// Sets if the caption is visible
e . DragUIOverride ( ) . IsCaptionVisible ( true ) ;
// Sets if the dragged content is visible
e . DragUIOverride ( ) . IsContentVisible ( false ) ;
// Sets if the glyph is visibile
e . DragUIOverride ( ) . IsGlyphVisible ( false ) ;
}
// -------------------------------- WinRT Events ---------------------------------
// Winrt events need a method for adding a callback to the event and removing the callback.
// These macros will define them both for you.
DEFINE_EVENT ( TermControl , TitleChanged , _titleChangedHandlers , TerminalControl : : TitleChangedEventArgs ) ;
DEFINE_EVENT ( TermControl , FontSizeChanged , _fontSizeChangedHandlers , TerminalControl : : FontSizeChangedEventArgs ) ;
DEFINE_EVENT ( TermControl , ScrollPositionChanged , _scrollPositionChangedHandlers , TerminalControl : : ScrollPositionChangedEventArgs ) ;
DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER ( TermControl , PasteFromClipboard , _clipboardPasteHandlers , TerminalControl : : TermControl , TerminalControl : : PasteFromClipboardEventArgs ) ;
DEFINE_EVENT_WITH_TYPED_EVENT_HANDLER ( TermControl , CopyToClipboard , _clipboardCopyHandlers , TerminalControl : : TermControl , TerminalControl : : CopyToClipboardEventArgs ) ;
// clang-format on
}