Files
terminal/src/cascadia/TerminalControl/ControlInteractivity.cpp
Dustin L. Howett c334f91f80 Move PointerId, focus and in-bounds handling down into Interactivity (#20017)
These do not notionally belong to TermControl; as we prepare to move
automatic scrolling down into the Interactivity layer (for
WpfTerminalControl to depend on,) we're going to need to track these in
Interactivity too.

This also converts the arguments from the winrt-specific
Foundation::Point to Core::Point, and simplifies some of the signatures
to no longer pass things we do not need to pass.

Right now, this should result in no functional change.

Some of the tests are sensitive to the fact that we never loaded
`SPI_GETWHEELSCROLLLINES`
and instead defaulted to `1`. They've been hammered back into shape.
2026-03-31 16:38:32 -07:00

801 lines
35 KiB
C++

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "ControlInteractivity.h"
#include <DefaultSettings.h>
#include <unicode.hpp>
#include <Utils.h>
#include "../../types/inc/GlyphWidth.hpp"
#include "../../types/inc/Utils.hpp"
#include "../../buffer/out/search.h"
#include "InteractivityAutomationPeer.h"
#include "ControlInteractivity.g.cpp"
using namespace ::Microsoft::Console::Types;
using namespace ::Microsoft::Console::VirtualTerminal;
using namespace ::Microsoft::Terminal::Core;
using namespace winrt::Windows::Graphics::Display;
using namespace winrt::Windows::System;
using namespace winrt::Windows::ApplicationModel::DataTransfer;
static constexpr unsigned int MAX_CLICK_COUNT = 3;
namespace winrt::Microsoft::Terminal::Control::implementation
{
std::atomic<uint64_t> ControlInteractivity::_nextId{ 1 };
static constexpr TerminalInput::MouseButtonState toInternalMouseState(const Control::MouseButtonState& state)
{
return TerminalInput::MouseButtonState{
WI_IsFlagSet(state, MouseButtonState::IsLeftButtonDown),
WI_IsFlagSet(state, MouseButtonState::IsMiddleButtonDown),
WI_IsFlagSet(state, MouseButtonState::IsRightButtonDown)
};
}
ControlInteractivity::ControlInteractivity(IControlSettings settings,
Control::IControlAppearance unfocusedAppearance,
TerminalConnection::ITerminalConnection connection) :
_touchAnchor{ std::nullopt },
_lastMouseClickTimestamp{},
_lastMouseClickPos{},
_selectionNeedsToBeCopied{ false }
{
_id = _nextId.fetch_add(1, std::memory_order_relaxed);
_core = winrt::make_self<ControlCore>(settings, unfocusedAppearance, connection);
_core->Attached([weakThis = get_weak()](auto&&, auto&&) {
if (auto self{ weakThis.get() })
{
self->Attached.raise(*self, nullptr);
}
});
// GH#14464: Mark mode and quick-edit (shift+arrow) selections update
// the selection through ControlCore, bypassing SetEndSelectionPoint.
// Listen for selection changes so _selectionNeedsToBeCopied is set
// for ALL selection types, not just mouse drag.
_core->UpdateSelectionMarkers([weakThis = get_weak()](auto&&, auto&&) {
if (auto self{ weakThis.get() })
{
if (self->_core->HasSelection())
{
self->_selectionNeedsToBeCopied = true;
}
}
});
}
uint64_t ControlInteractivity::Id()
{
return _id;
}
void ControlInteractivity::Detach()
{
if (_uiaEngine)
{
// There's a potential race here where we've removed the TermControl
// from the UI tree, but the UIA engine is in the middle of a paint,
// and the UIA engine will try to dispatch to the
// TermControlAutomationPeer, which (is now)/(will very soon be) gone.
//
// To alleviate, make sure to disable the UIA engine and remove it,
// and ALSO disable the renderer. Core.Detach will take care of the
// TriggerTeardown (which will stop the renderer
// after all current engines are done painting).
//
// Simply disabling the UIA engine is not enough, because it's
// possible that it had already started presenting here.
LOG_IF_FAILED(_uiaEngine->Disable());
_core->DetachUiaEngine(_uiaEngine.get());
}
_core->Detach();
}
void ControlInteractivity::AttachToNewControl()
{
_core->AttachToNewControl();
}
// Method Description:
// - Updates our internal settings. These settings should be
// interactivity-specific. Right now, we primarily update _rowsToScroll
// with the current value of SPI_GETWHEELSCROLLLINES.
// Arguments:
// - <none>
// Return Value:
// - <none>
void ControlInteractivity::UpdateSettings()
{
_updateSystemParameterSettings();
}
void ControlInteractivity::Initialize()
{
_updateSystemParameterSettings();
// import value from WinUser (convert from milli-seconds to micro-seconds)
_multiClickTimer = GetDoubleClickTime() * 1000;
}
Control::ControlCore ControlInteractivity::Core()
{
return *_core;
}
void ControlInteractivity::Close()
{
Closed.raise(*this, nullptr);
if (_core)
{
_core->Close();
}
}
// Method Description:
// - Returns the number of clicks that occurred (double and triple click support).
// Every call to this function registers a click.
// 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
unsigned int ControlInteractivity::_numberOfClicks(Core::Point clickPos,
Timestamp clickTime)
{
// if click occurred at a different location or past the multiClickTimer...
Timestamp delta;
THROW_IF_FAILED(UInt64Sub(clickTime, _lastMouseClickTimestamp, &delta));
if (clickPos != _lastMouseClickPos || delta > _multiClickTimer)
{
_multiClickCounter = 1;
}
else
{
_multiClickCounter++;
}
_lastMouseClickTimestamp = clickTime;
_lastMouseClickPos = clickPos;
return _multiClickCounter;
}
void ControlInteractivity::GotFocus()
{
_focused = true;
if (_uiaEngine.get())
{
THROW_IF_FAILED(_uiaEngine->Enable());
}
_core->GotFocus();
_updateSystemParameterSettings();
}
void ControlInteractivity::LostFocus()
{
_focused = false;
if (_uiaEngine.get())
{
THROW_IF_FAILED(_uiaEngine->Disable());
}
_core->LostFocus();
}
// Method Description
// - Updates internal params based on system parameters
void ControlInteractivity::_updateSystemParameterSettings() noexcept
{
if (!SystemParametersInfoW(SPI_GETWHEELSCROLLLINES, 0, &_rowsToScroll, 0))
{
LOG_LAST_ERROR();
// If SystemParametersInfoW fails, which it shouldn't, fall back to
// Windows' default value.
_rowsToScroll = 3;
}
}
// Method Description:
// - Given a copy-able selection, get the selected text from the buffer and send it to the
// Windows Clipboard (CascadiaWin32:main.cpp).
// Arguments:
// - singleLine: collapse all of the text to one line
// - withControlSequences: if enabled, the copied plain text contains color/style ANSI escape codes from the selection
// - formats: which formats to copy (defined by action's CopyFormatting arg). nullptr
// if we should defer which formats are copied to the global setting
bool ControlInteractivity::CopySelectionToClipboard(bool singleLine,
bool withControlSequences,
const CopyFormat formats)
{
if (_core)
{
// Return false if there's no selection to copy. If there's no
// selection, returning false will indicate that the actions that
// triggered this should _not_ be marked as handled, so ctrl+c
// without a selection can still send ^C
if (!_core->HasSelection())
{
return false;
}
// Mark the current selection as copied
_selectionNeedsToBeCopied = false;
return _core->CopySelectionToClipboard(singleLine, withControlSequences, formats);
}
return false;
}
// Method Description:
// - Initiate a paste operation.
void ControlInteractivity::RequestPasteTextFromClipboard()
{
auto args = winrt::make<PasteFromClipboardEventArgs>(
[core = _core](const winrt::hstring& wstr) {
core->PasteText(wstr);
},
_core->BracketedPasteEnabled());
// send paste event up to TermApp
PasteFromClipboard.raise(*this, std::move(args));
}
void ControlInteractivity::PointerPressed(const uint32_t /*pointerId*/,
Control::MouseButtonState buttonState,
const unsigned int pointerUpdateKind,
const uint64_t timestamp,
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
const Core::Point pixelPosition)
{
// Un-rounded coordinates; we only round when selecting text
const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false);
const auto altEnabled = modifiers.IsAltPressed();
const auto shiftEnabled = modifiers.IsShiftPressed();
const auto ctrlEnabled = modifiers.IsCtrlPressed();
// Mark that this pointer event actually started within our bounds.
// We'll need this later, for PointerMoved events.
_pointerPressedInBounds = true;
// GH#9396: we prioritize hyper-link over VT mouse events
auto hyperlink = _core->GetHyperlink(terminalPosition.to_core_point());
if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown) &&
ctrlEnabled &&
!hyperlink.empty())
{
const auto clickCount = _numberOfClicks(pixelPosition, timestamp);
// Handle hyper-link only on the first click to prevent multiple activations
if (clickCount == 1)
{
_hyperlinkHandler(hyperlink);
}
}
else if (_canSendVTMouseInput(modifiers))
{
_sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState);
}
else if (WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown))
{
const auto clickCount = _numberOfClicks(pixelPosition, 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 auto multiClickMapper = clickCount > MAX_CLICK_COUNT ? ((clickCount + MAX_CLICK_COUNT - 1) % MAX_CLICK_COUNT) + 1 : clickCount;
// Capture the position of the first click when no selection is active
if (multiClickMapper == 1)
{
_singleClickTouchdownPos = pixelPosition;
if (!_core->HasSelection())
{
_lastMouseClickPosNoSelection = pixelPosition;
}
}
const auto isOnOriginalPosition = _lastMouseClickPosNoSelection == pixelPosition;
// Rounded coordinates for text selection.
// Don't round in VT mouse mode; cell-level precision matters more
const auto round = !_core->IsVtMouseModeEnabled();
_core->LeftClickOnTerminal(_getTerminalPosition(til::point{ pixelPosition }, round),
multiClickMapper,
altEnabled,
shiftEnabled,
isOnOriginalPosition,
_selectionNeedsToBeCopied);
if (_core->HasSelection())
{
// GH#9787: if selection is active we don't want to track the touchdown position
// so that dragging the mouse will extend the selection rather than starting the new one.
// In VT mouse mode, keep tracking the touchdown point so that PointerMoved
// can re-anchor the selection based on drag direction (the dx < 0 adjustment).
// Without this, dragging left wouldn't include the initially clicked cell
// because floored coordinates place the anchor on the cell's left edge.
if (!_core->IsVtMouseModeEnabled())
{
_singleClickTouchdownPos = std::nullopt;
}
}
}
else if (WI_IsFlagSet(buttonState, MouseButtonState::IsRightButtonDown))
{
if (_core->Settings().RightClickContextMenu())
{
// Let the core know we're about to open a menu here. It has
// some separate conditional logic based on _where_ the user
// wanted to open the menu.
_core->AnchorContextMenu(terminalPosition);
auto contextArgs = winrt::make<ContextMenuRequestedEventArgs>(til::point{ pixelPosition }.to_winrt_point());
ContextMenuRequested.raise(*this, contextArgs);
}
else
{
// GH#19942, GH#14464: Don't re-copy a selection that was
// already copied via copyOnSelect on mouse-up. But DO copy
// if the selection was made via mark mode or modified with
// quick-edit keys (shift+arrow), since those paths never
// triggered an automatic copy.
const auto copied = (_selectionNeedsToBeCopied || !_core->CopyOnSelect()) &&
CopySelectionToClipboard(shiftEnabled, false, _core->Settings().CopyFormatting());
_core->ClearSelection();
if (_core->CopyOnSelect() || !copied)
{
// CopyOnSelect: right-click always pastes.
// Otherwise: no selection → paste.
RequestPasteTextFromClipboard();
}
}
}
}
void ControlInteractivity::TouchPressed(const Core::Point contactPoint)
{
_touchAnchor = contactPoint;
}
bool ControlInteractivity::PointerMoved(const uint32_t /*pointerId*/,
Control::MouseButtonState buttonState,
const unsigned int pointerUpdateKind,
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
const Core::Point pixelPosition)
{
const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false);
// Returning true from this function indicates that the caller should do no further processing of this movement.
bool handledCompletely = false;
// Short-circuit isReadOnly check to avoid warning dialog
if (_focused && !_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers))
{
_sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState);
handledCompletely = true;
}
// GH#4603 - don't modify the selection if the pointer press didn't
// actually start _in_ the control bounds. Case in point - someone drags
// a file into the bounds of the control. That shouldn't send the
// selection into space.
else if (_focused && _pointerPressedInBounds && WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown))
{
if (_singleClickTouchdownPos)
{
// Figure out if the user's moved a 1/4th of a cell's smaller axis
// (practically always the width) away from the clickdown point.
const auto fontSizeInDips = _core->FontSizeInDips();
const auto touchdownPoint = *_singleClickTouchdownPos;
const auto dx = pixelPosition.X - touchdownPoint.X;
const auto dy = pixelPosition.Y - touchdownPoint.Y;
const auto w = fontSizeInDips.Width;
const auto distanceSquared = dx * dx + dy * dy;
const auto maxDistanceSquared = w * w / 16; // (w / 4)^2
if (distanceSquared >= maxDistanceSquared)
{
// GH#9955.c: Make sure to use the terminal location of the
// _touchdown_ point here. We want to start the selection
// from where the user initially clicked, not where they are
// now.
auto termPos = _getTerminalPosition(til::point{ touchdownPoint }, false);
if (dx < 0)
{
// _getTerminalPosition(_, false) will floor the x-value,
// meaning that the selection will start on the left-side
// of the current cell. This is great if the use is dragging
// towards the right.
// If the user is dragging towards the left (dx < 0),
// we want to select the current cell, so place the anchor on the right
// side of the current cell.
termPos.x++;
}
_core->SetSelectionAnchor(termPos);
// stop tracking the touchdown point
_singleClickTouchdownPos = std::nullopt;
}
}
SetEndSelectionPoint(pixelPosition);
}
_core->SetHoveredCell(terminalPosition.to_core_point());
return handledCompletely;
}
void ControlInteractivity::TouchMoved(const Core::Point newTouchPoint)
{
if (_focused &&
_touchAnchor)
{
const auto anchor = _touchAnchor.value();
// Our actualFont's size is in pixels, convert to DIPs, which the
// rest of the Points here are in.
const auto fontSizeInDips{ _core->FontSizeInDips() };
// Get the difference between the point we've dragged to and the start of the touch.
const auto dy = static_cast<float>(newTouchPoint.Y - anchor.Y);
// Start viewport scroll after we've moved more than a half row of text
if (std::abs(dy) > (fontSizeInDips.Height / 2.0f))
{
// 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 auto numRows = dy / -fontSizeInDips.Height;
const auto currentOffset = _core->ScrollOffset();
const auto newValue = numRows + currentOffset;
// Update the Core's viewport position, and raise a
// ScrollPositionChanged event to update the scrollbar
UpdateScrollbar(newValue);
// Use this point as our new scroll anchor.
_touchAnchor = newTouchPoint;
}
}
}
void ControlInteractivity::PointerReleased(const uint32_t /*pointerId*/,
Control::MouseButtonState buttonState,
const unsigned int pointerUpdateKind,
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
const Core::Point pixelPosition)
{
_pointerPressedInBounds = false;
const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false);
// Short-circuit isReadOnly check to avoid warning dialog
if (!_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers))
{
_sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState);
return;
}
// Only a left click release when copy on select is active should perform a copy.
// Right clicks and middle clicks should not need to do anything when released.
const auto isLeftMouseRelease = pointerUpdateKind == WM_LBUTTONUP;
if (_core->CopyOnSelect() &&
isLeftMouseRelease &&
_selectionNeedsToBeCopied)
{
// IMPORTANT!
// DO NOT clear the selection here!
// Otherwise, the selection will be cleared immediately after you make it.
CopySelectionToClipboard(false, false, _core->Settings().CopyFormatting());
}
_singleClickTouchdownPos = std::nullopt;
}
void ControlInteractivity::TouchReleased()
{
_touchAnchor = std::nullopt;
}
// Method Description:
// - Actually handle a scrolling event, whether from a mouse wheel or a
// touchpad scroll. Depending upon what modifier keys are pressed,
// different actions will take place.
// * Attempts to first dispatch the mouse scroll as a VT event
// * If Ctrl+Shift are pressed, then attempts to change our opacity
// * If just Ctrl is pressed, we'll attempt to "zoom" by changing our font size
// * Otherwise, just scrolls the content of the viewport
// Arguments:
// - point: the location of the mouse during this event
// - modifiers: The modifiers pressed during this event, in the form of a VirtualKeyModifiers
// - delta: the mouse wheel delta that triggered this event.
bool ControlInteractivity::MouseWheel(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
const Core::Point delta,
const Core::Point pixelPosition,
const Control::MouseButtonState buttonState)
{
const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition }, false);
// Short-circuit isReadOnly check to avoid warning dialog.
//
// GH#3321: Alternate scroll mode is a special type of mouse input mode
// where the terminal sends arrow keys when the user mouse wheels, but
// the client app doesn't care for other mouse input. It's tracked
// separately from _canSendVTMouseInput.
if (!_core->IsInReadOnlyMode() &&
(_canSendVTMouseInput(modifiers) || _shouldSendAlternateScroll(modifiers, delta)))
{
// Most mouse event handlers call
// _trySendMouseEvent(point);
// here with a PointerPoint. However, as of #979, we don't have a
// PointerPoint to work with. So, we're just going to do a
// mousewheel event manually
return _sendMouseEventHelper(terminalPosition,
delta.Y != 0 ? WM_MOUSEWHEEL : WM_MOUSEHWHEEL,
modifiers,
::base::saturated_cast<short>(delta.Y != 0 ? delta.Y : delta.X),
buttonState);
}
const auto ctrlPressed = modifiers.IsCtrlPressed();
const auto shiftPressed = modifiers.IsShiftPressed();
if (ctrlPressed && shiftPressed && _core->Settings().ScrollToChangeOpacity())
{
_mouseTransparencyHandler(delta.Y);
}
else if (ctrlPressed && !shiftPressed && _core->Settings().ScrollToZoom())
{
_mouseZoomHandler(delta.Y);
}
else
{
_mouseScrollHandler(delta.Y, pixelPosition, WI_IsFlagSet(buttonState, MouseButtonState::IsLeftButtonDown));
}
return false;
}
// 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 ControlInteractivity::_mouseTransparencyHandler(const int32_t mouseDelta) const
{
// Transparency is on a scale of [0.0,1.0], so only increment by .01.
const auto effectiveDelta = mouseDelta < 0 ? -.01f : .01f;
_core->AdjustOpacity(effectiveDelta);
}
// 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 ControlInteractivity::_mouseZoomHandler(const int32_t mouseDelta) const
{
const auto fontDelta = mouseDelta < 0 ? -1.0f : 1.0f;
_core->AdjustFontSize(fontDelta);
}
// Method Description:
// - Scroll the visible viewport in response to a mouse wheel event.
// Arguments:
// - mouseDelta: the mouse wheel delta that triggered this event.
// - pixelPosition: the location of the mouse during this event
// - isLeftButtonPressed: true iff the left mouse button was pressed during this event.
void ControlInteractivity::_mouseScrollHandler(const int32_t mouseDelta,
const Core::Point pixelPosition,
const bool isLeftButtonPressed)
{
// GH#9955.b: Start scrolling from our internal scrollbar position. This
// lets us accumulate fractional numbers of rows to scroll with each
// event. Especially for precision trackpads, we might be getting scroll
// deltas smaller than a single row, but we still want lots of those to
// accumulate.
//
// At the start, let's compare what we _think_ the scrollbar is, with
// what it should be. It's possible the core scrolled out from
// underneath us. We wouldn't know - we don't want the overhead of
// another ScrollPositionChanged handler. If the scrollbar should be
// somewhere other than where it is currently, then start from that row.
const auto currentInternalRow = std::lround(_internalScrollbarPosition);
const auto currentCoreRow = _core->ScrollOffset();
const auto currentOffset = currentInternalRow == currentCoreRow ?
_internalScrollbarPosition :
currentCoreRow;
// negative = down, positive = up
// However, for us, the signs are flipped.
// With one of the precision mice, one click is always a multiple of 120 (WHEEL_DELTA),
// but the "smooth scrolling" mode results in non-int values
const auto rowDelta = mouseDelta / (-1.0f * WHEEL_DELTA);
// WHEEL_PAGESCROLL is a Win32 constant that represents the "scroll one page
// at a time" setting. If we ignore it, we will scroll a truly absurd number
// of rows.
const auto rowsToScroll{ _rowsToScroll == WHEEL_PAGESCROLL ? _core->ViewHeight() : _rowsToScroll };
const auto newValue = rowsToScroll * rowDelta + currentOffset;
// Update the Core's viewport position, and raise a
// ScrollPositionChanged event to update the scrollbar
UpdateScrollbar(newValue);
if (isLeftButtonPressed)
{
// If user is mouse selecting and scrolls, they then point at new
// character. Make sure selection reflects that immediately.
SetEndSelectionPoint(pixelPosition);
}
}
// Method Description:
// - Update the scroll position in such a way that should update the
// scrollbar. For example, when scrolling the buffer with the mouse or
// touch input. This will both update the Core's Terminal's buffer
// location, then also raise our own ScrollPositionChanged event.
// UserScrollViewport _won't_ raise the core's ScrollPositionChanged
// event, because it's assumed that's already being called from a context
// that knows about the change to the scrollbar. So we need to raise the
// event on our own.
// - The hosting control should make sure to listen to our own
// ScrollPositionChanged event and use that as an opportunity to update
// the location of the scrollbar.
// Arguments:
// - newValue: The new top of the viewport
// Return Value:
// - <none>
void ControlInteractivity::UpdateScrollbar(const float newValue)
{
// Set this as the new value of our internal scrollbar representation.
// We're doing this so we can accumulate fractional amounts of a row to
// scroll each time the mouse scrolls.
_internalScrollbarPosition = std::clamp(newValue, 0.0f, static_cast<float>(_core->BufferHeight()));
// If the new scrollbar position, rounded to an int, is at a different
// row, then actually update the scroll position in the core, and raise
// a ScrollPositionChanged to inform the control.
const auto viewTop = std::lround(_internalScrollbarPosition);
if (viewTop != _core->ScrollOffset())
{
_core->UserScrollViewport(viewTop);
// _core->ScrollOffset() is now set to newValue
ScrollPositionChanged.raise(*this,
winrt::make<ScrollPositionChangedArgs>(_core->ScrollOffset(),
_core->ViewHeight(),
_core->BufferHeight()));
}
}
void ControlInteractivity::_hyperlinkHandler(const std::wstring_view uri)
{
OpenHyperlink.raise(*this, winrt::make<OpenHyperlinkEventArgs>(winrt::hstring{ uri }));
}
bool ControlInteractivity::_canSendVTMouseInput(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers)
{
// If the user is holding down Shift, suppress mouse events
// TODO GH#4875: disable/customize this functionality
if (modifiers.IsShiftPressed())
{
return false;
}
return _core->IsVtMouseModeEnabled();
}
bool ControlInteractivity::_shouldSendAlternateScroll(const ::Microsoft::Terminal::Core::ControlKeyStates modifiers, const Core::Point delta)
{
// If the user is holding down Shift, suppress mouse events
// TODO GH#4875: disable/customize this functionality
if (modifiers.IsShiftPressed())
{
return false;
}
if (delta.Y != 0)
{
return _core->ShouldSendAlternateScroll(WM_MOUSEWHEEL, delta.Y);
}
else
{
return _core->ShouldSendAlternateScroll(WM_MOUSEHWHEEL, delta.X);
}
}
// 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 ControlInteractivity::SetEndSelectionPoint(const Core::Point pixelPosition)
{
// Don't round in VT mouse mode; cell-level precision matters more
const auto round = !_core->IsVtMouseModeEnabled();
_core->SetEndSelectionPoint(_getTerminalPosition(til::point{ pixelPosition }, round));
_selectionNeedsToBeCopied = true;
}
// Method Description:
// - Gets the corresponding viewport terminal position for the point in
// pixels, by normalizing with the font size.
// Arguments:
// - pixelPosition: the (x,y) position of a given point (i.e.: mouse cursor).
// NOTE: origin (0,0) is top-left.
// - roundToNearestCell: if true, round the x-value. Otherwise, floor it (standard int division)
// Return Value:
// - the corresponding viewport terminal position for the given Point parameter
til::point ControlInteractivity::_getTerminalPosition(const til::point pixelPosition, bool roundToNearestCell)
{
// Get the size of the font, which is in pixels
const auto fontSize{ _core->GetFont().GetSize() };
if (roundToNearestCell)
{
// GH#5099: round the x-value to the nearest cell
til::point result;
result.x = gsl::narrow_cast<til::CoordType>(std::round(gsl::narrow_cast<double>(pixelPosition.x) / fontSize.width));
result.y = pixelPosition.y / fontSize.height;
return result;
}
// Convert the location in pixels to characters within the current viewport.
return pixelPosition / fontSize;
}
bool ControlInteractivity::_sendMouseEventHelper(const til::point terminalPosition,
const unsigned int pointerUpdateKind,
const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
const SHORT wheelDelta,
Control::MouseButtonState buttonState)
{
const auto adjustment = _core->BufferHeight() - _core->ScrollOffset() - _core->ViewHeight();
// If the click happened outside the active region, core should get a chance to filter it out or clamp it.
const auto adjustedY = terminalPosition.y - adjustment;
return _core->SendMouseEvent({ terminalPosition.x, adjustedY }, pointerUpdateKind, modifiers, wheelDelta, toInternalMouseState(buttonState));
}
// Method Description:
// - Creates an automation peer for the Terminal Control, enabling
// accessibility on our control.
// - Our implementation implements the ITextProvider pattern, and the
// IControlAccessibilityInfo, to connect to the UiaEngine, which must be
// attached to the core's renderer.
// - The TermControlAutomationPeer will connect this to the UI tree.
// Arguments:
// - None
// Return Value:
// - The automation peer for our control
Control::InteractivityAutomationPeer ControlInteractivity::OnCreateAutomationPeer()
try
{
const auto autoPeer = winrt::make_self<implementation::InteractivityAutomationPeer>(this);
if (_uiaEngine)
{
_core->DetachUiaEngine(_uiaEngine.get());
}
_uiaEngine = std::make_unique<::Microsoft::Console::Render::UiaEngine>(autoPeer.get());
_core->AttachUiaEngine(_uiaEngine.get());
return *autoPeer;
}
catch (...)
{
LOG_CAUGHT_EXCEPTION();
return nullptr;
}
::Microsoft::Console::Render::IRenderData* ControlInteractivity::GetRenderData() const
{
return _core->GetRenderData();
}
}