diff --git a/src/cascadia/TerminalControl/ControlCore.cpp b/src/cascadia/TerminalControl/ControlCore.cpp index 6a6b8a670f8..19d66932227 100644 --- a/src/cascadia/TerminalControl/ControlCore.cpp +++ b/src/cascadia/TerminalControl/ControlCore.cpp @@ -421,7 +421,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { auto lock = _terminal->LockForWriting(); _terminal->SelectAll(); - _renderer->TriggerSelection(); + _updateSelection(); return true; } @@ -430,7 +430,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { auto lock = _terminal->LockForWriting(); _terminal->UpdateSelection(updateSlnParams->first, updateSlnParams->second, modifiers); - _renderer->TriggerSelection(); + _updateSelection(); return true; } @@ -438,7 +438,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (!modifiers.IsWinPressed()) { _terminal->ClearSelection(); - _renderer->TriggerSelection(); + _updateSelection(); } // When there is a selection active, escape should clear it and NOT flow through @@ -934,6 +934,31 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetSelectionAnchor(position); } + // Method Description: + // - Retrieves selection metadata from Terminal necessary to draw the + // selection markers. + // - Since all of this needs to be done under lock, it is more performant + // to throw it all in a struct and pass it along. + Control::SelectionData ControlCore::SelectionInfo() const + { + auto lock = _terminal->LockForReading(); + Control::SelectionData info; + + const auto start{ _terminal->SelectionStartForRendering() }; + info.StartPos = { start.X, start.Y }; + + const auto end{ _terminal->SelectionEndForRendering() }; + info.EndPos = { end.X, end.Y }; + + info.MovingEnd = _terminal->MovingEnd(); + info.MovingCursor = _terminal->MovingCursor(); + + const auto bufferSize{ _terminal->GetTextBuffer().GetSize() }; + info.StartAtLeftBoundary = _terminal->GetSelectionAnchor().x == bufferSize.Left(); + info.EndAtRightBoundary = _terminal->GetSelectionEnd().x == bufferSize.RightInclusive(); + return info; + } + // Method Description: // - Sets selection's end position to match supplied cursor position, e.g. while mouse dragging. // Arguments: @@ -957,6 +982,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation // save location (for rendering) + render _terminal->SetSelectionEnd(terminalPosition); _renderer->TriggerSelection(); + + // this is used for mouse dragging, + // so hide the markers + _UpdateSelectionMarkersHandlers(*this, winrt::make(true)); } // Called when the Terminal wants to set something to the clipboard, i.e. @@ -1018,7 +1047,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation if (!_settings->CopyOnSelect()) { _terminal->ClearSelection(); - _renderer->TriggerSelection(); + _updateSelection(); } // send data up for clipboard @@ -1034,7 +1063,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { auto lock = _terminal->LockForWriting(); _terminal->SelectAll(); - _renderer->TriggerSelection(); + _updateSelection(); } bool ControlCore::ToggleBlockSelection() @@ -1044,6 +1073,9 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _terminal->SetBlockSelection(!_terminal->IsBlockSelection()); _renderer->TriggerSelection(); + // do not update the selection markers! + // if we were showing them, keep it that way. + // otherwise, continue to not show them return true; } return false; @@ -1053,7 +1085,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { auto lock = _terminal->LockForWriting(); _terminal->ToggleMarkMode(); - _renderer->TriggerSelection(); + _updateSelection(); } bool ControlCore::IsInMarkMode() const @@ -1068,7 +1100,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation { _terminal->WritePastedText(hstr); _terminal->ClearSelection(); - _renderer->TriggerSelection(); + _updateSelection(); _terminal->TrySnapOnInput(); } @@ -1389,6 +1421,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->SetBlockSelection(false); search.Select(); _renderer->TriggerSelection(); + + // this is used for search, + // so hide the markers + _UpdateSelectionMarkersHandlers(*this, winrt::make(true)); } // Raise a FoundMatch event, which the control will use to notify @@ -1589,8 +1625,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation _terminal->MultiClickSelection(terminalPosition, mode); selectionNeedsToBeCopied = true; } + _updateSelection(); + } + void ControlCore::_updateSelection() + { _renderer->TriggerSelection(); + const bool clearMarkers{ !_terminal->IsSelectionActive() }; + _UpdateSelectionMarkersHandlers(*this, winrt::make(clearMarkers)); } void ControlCore::AttachUiaEngine(::Microsoft::Console::Render::IRenderEngine* const pEngine) diff --git a/src/cascadia/TerminalControl/ControlCore.h b/src/cascadia/TerminalControl/ControlCore.h index 0dcfde33359..e11bd19772a 100644 --- a/src/cascadia/TerminalControl/ControlCore.h +++ b/src/cascadia/TerminalControl/ControlCore.h @@ -159,6 +159,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool HasSelection() const; bool CopyOnSelect() const; Windows::Foundation::Collections::IVector SelectedText(bool trimTrailingWhitespace) const; + Control::SelectionData SelectionInfo() const; void SetSelectionAnchor(const til::point position); void SetEndSelectionPoint(const til::point position); @@ -214,6 +215,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation TYPED_EVENT(ReceivedOutput, IInspectable, IInspectable); TYPED_EVENT(FoundMatch, IInspectable, Control::FoundResultsArgs); TYPED_EVENT(ShowWindowChanged, IInspectable, Control::ShowWindowArgs); + TYPED_EVENT(UpdateSelectionMarkers, IInspectable, Control::UpdateSelectionMarkersEventArgs); // clang-format on private: @@ -269,6 +271,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation bool _setFontSizeUnderLock(int fontSize); void _updateFont(const bool initialUpdate = false); void _refreshSizeUnderLock(); + void _updateSelection(); void _sendInputToConnection(std::wstring_view wstr); diff --git a/src/cascadia/TerminalControl/ControlCore.idl b/src/cascadia/TerminalControl/ControlCore.idl index 13bd83c8550..1dc39842f97 100644 --- a/src/cascadia/TerminalControl/ControlCore.idl +++ b/src/cascadia/TerminalControl/ControlCore.idl @@ -29,6 +29,16 @@ namespace Microsoft.Terminal.Control All }; + struct SelectionData + { + Microsoft.Terminal.Core.Point StartPos; + Microsoft.Terminal.Core.Point EndPos; + Boolean MovingEnd; + Boolean MovingCursor; + Boolean StartAtLeftBoundary; + Boolean EndAtRightBoundary; + }; + [default_interface] runtimeclass ControlCore : ICoreState { ControlCore(IControlSettings settings, @@ -90,6 +100,7 @@ namespace Microsoft.Terminal.Control Boolean HasSelection { get; }; IVector SelectedText(Boolean trimTrailingWhitespace); + SelectionData SelectionInfo { get; }; String HoveredUriText { get; }; Windows.Foundation.IReference HoveredCell { get; }; @@ -125,6 +136,7 @@ namespace Microsoft.Terminal.Control event Windows.Foundation.TypedEventHandler ReceivedOutput; event Windows.Foundation.TypedEventHandler FoundMatch; event Windows.Foundation.TypedEventHandler ShowWindowChanged; + event Windows.Foundation.TypedEventHandler UpdateSelectionMarkers; }; } diff --git a/src/cascadia/TerminalControl/EventArgs.cpp b/src/cascadia/TerminalControl/EventArgs.cpp index 665ea17f448..9f9b41ac1f7 100644 --- a/src/cascadia/TerminalControl/EventArgs.cpp +++ b/src/cascadia/TerminalControl/EventArgs.cpp @@ -13,3 +13,4 @@ #include "TransparencyChangedEventArgs.g.cpp" #include "FoundResultsArgs.g.cpp" #include "ShowWindowArgs.g.cpp" +#include "UpdateSelectionMarkersEventArgs.g.cpp" diff --git a/src/cascadia/TerminalControl/EventArgs.h b/src/cascadia/TerminalControl/EventArgs.h index 0d7c6d4a005..a542e391f63 100644 --- a/src/cascadia/TerminalControl/EventArgs.h +++ b/src/cascadia/TerminalControl/EventArgs.h @@ -13,6 +13,7 @@ #include "TransparencyChangedEventArgs.g.h" #include "FoundResultsArgs.g.h" #include "ShowWindowArgs.g.h" +#include "UpdateSelectionMarkersEventArgs.g.h" namespace winrt::Microsoft::Terminal::Control::implementation { @@ -157,4 +158,15 @@ namespace winrt::Microsoft::Terminal::Control::implementation WINRT_PROPERTY(bool, ShowOrHide); }; + + struct UpdateSelectionMarkersEventArgs : public UpdateSelectionMarkersEventArgsT + { + public: + UpdateSelectionMarkersEventArgs(const bool clearMarkers) : + _ClearMarkers(clearMarkers) + { + } + + WINRT_PROPERTY(bool, ClearMarkers, false); + }; } diff --git a/src/cascadia/TerminalControl/EventArgs.idl b/src/cascadia/TerminalControl/EventArgs.idl index 62ed095c8f4..941384c5b05 100644 --- a/src/cascadia/TerminalControl/EventArgs.idl +++ b/src/cascadia/TerminalControl/EventArgs.idl @@ -69,7 +69,6 @@ namespace Microsoft.Terminal.Control Double Opacity { get; }; } - runtimeclass FoundResultsArgs { Boolean FoundMatch { get; }; @@ -79,4 +78,9 @@ namespace Microsoft.Terminal.Control { Boolean ShowOrHide { get; }; } + + runtimeclass UpdateSelectionMarkersEventArgs + { + Boolean ClearMarkers { get; }; + } } diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp index 53a8f9f7785..fb44871fdd6 100644 --- a/src/cascadia/TerminalControl/TermControl.cpp +++ b/src/cascadia/TerminalControl/TermControl.cpp @@ -83,6 +83,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.RaiseNotice({ this, &TermControl::_coreRaisedNotice }); _core.HoveredHyperlinkChanged({ this, &TermControl::_hoveredHyperlinkChanged }); _core.FoundMatch({ this, &TermControl::_coreFoundMatch }); + _core.UpdateSelectionMarkers({ this, &TermControl::_updateSelectionMarkers }); _interactivity.OpenHyperlink({ this, &TermControl::_HyperlinkHandler }); _interactivity.ScrollPositionChanged({ this, &TermControl::_ScrollPositionChanged }); @@ -358,6 +359,11 @@ namespace winrt::Microsoft::Terminal::Control::implementation // switch from a solid color brush to an acrylic one. _changeBackgroundColor(bg); + // Update selection markers + Windows::UI::Xaml::Media::SolidColorBrush cursorColorBrush{ til::color{ newAppearance.CursorColor() } }; + SelectionStartMarker().Fill(cursorColorBrush); + SelectionEndMarker().Fill(cursorColorBrush); + // Set TSF Foreground Media::SolidColorBrush foregroundBrush{}; if (_core.Settings().UseBackgroundImageForWindow()) @@ -1831,6 +1837,13 @@ namespace winrt::Microsoft::Terminal::Control::implementation update.newValue = args.ViewTop(); _updateScrollBar->Run(update); + + // if a selection marker is already visible, + // update the position of those markers + if (SelectionStartMarker().Visibility() == Visibility::Visible || SelectionEndMarker().Visibility() == Visibility::Visible) + { + _updateSelectionMarkers(nullptr, winrt::make(false)); + } } // Method Description: @@ -2735,8 +2748,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation _core.ClearHoveredCell(); } - winrt::fire_and_forget TermControl::_hoveredHyperlinkChanged(IInspectable sender, - IInspectable args) + winrt::fire_and_forget TermControl::_hoveredHyperlinkChanged(IInspectable /*sender*/, + IInspectable /*args*/) { auto weakThis{ get_weak() }; co_await wil::resume_foreground(Dispatcher()); @@ -2763,12 +2776,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation HyperlinkTooltipBorder().BorderThickness(newThickness); // Compute the location of the top left corner of the cell in DIPS - const til::size marginsInDips{ til::math::rounding, GetPadding().Left, GetPadding().Top }; - const til::point startPos{ lastHoveredCell.Value() }; - const til::size fontSize{ til::math::rounding, _core.FontSize() }; - const auto posInPixels{ startPos * fontSize }; - const til::point posInDIPs{ til::math::flooring, posInPixels.x / scale, posInPixels.y / scale }; - const auto locationInDIPs{ posInDIPs + marginsInDips }; + const til::point locationInDIPs{ _toPosInDips(lastHoveredCell.Value()) }; // Move the border to the top left corner of the cell OverlayCanvas().SetLeft(HyperlinkTooltipBorder(), locationInDIPs.x - offset.x); @@ -2778,10 +2786,117 @@ namespace winrt::Microsoft::Terminal::Control::implementation } } + winrt::fire_and_forget TermControl::_updateSelectionMarkers(IInspectable /*sender*/, Control::UpdateSelectionMarkersEventArgs args) + { + auto weakThis{ get_weak() }; + co_await resume_foreground(Dispatcher()); + if (weakThis.get() && args) + { + if (_core.HasSelection() && !args.ClearMarkers()) + { + // retrieve all of the necessary selection marker data + // from the TerminalCore layer under one lock to improve performance + const auto markerData{ _core.SelectionInfo() }; + + // lambda helper function that can be used to display a selection marker + // - targetEnd: if true, target the "end" selection marker. Otherwise, target "start". + auto displayMarker = [&](bool targetEnd) { + const auto flipMarker{ targetEnd ? markerData.EndAtRightBoundary : markerData.StartAtLeftBoundary }; + const auto& marker{ targetEnd ? SelectionEndMarker() : SelectionStartMarker() }; + + // Ensure the marker is oriented properly + // (i.e. if start is at the beginning of the buffer, it should be flipped) + auto transform{ marker.RenderTransform().as() }; + transform.ScaleX(std::abs(transform.ScaleX()) * (flipMarker ? -1.0 : 1.0)); + marker.RenderTransform(transform); + + // Compute the location of the top left corner of the cell in DIPS + auto terminalPos{ targetEnd ? markerData.EndPos : markerData.StartPos }; + if (flipMarker) + { + // When we flip the marker, a negative scaling makes us be one cell-width to the left. + // Add one to the viewport pos' x-coord to fix that. + terminalPos.X += 1; + } + const til::point locationInDIPs{ _toPosInDips(terminalPos) }; + + // Move the marker to the top left corner of the cell + SelectionCanvas().SetLeft(marker, + (locationInDIPs.x - SwapChainPanel().ActualOffset().x)); + SelectionCanvas().SetTop(marker, + (locationInDIPs.y - SwapChainPanel().ActualOffset().y)); + marker.Visibility(Visibility::Visible); + }; + + // show/update selection markers + // figure out which endpoint to move, get it and the relevant icon (hide the other icon) + const auto selectionAnchor{ markerData.MovingEnd ? markerData.EndPos : markerData.StartPos }; + const auto& marker{ markerData.MovingEnd ? SelectionEndMarker() : SelectionStartMarker() }; + const auto& otherMarker{ markerData.MovingEnd ? SelectionStartMarker() : SelectionEndMarker() }; + if (selectionAnchor.Y < 0 || selectionAnchor.Y >= _core.ViewHeight()) + { + // if the endpoint is outside of the viewport, + // just hide the markers + marker.Visibility(Visibility::Collapsed); + otherMarker.Visibility(Visibility::Collapsed); + co_return; + } + else if (markerData.MovingCursor) + { + // display both markers + displayMarker(true); + displayMarker(false); + } + else + { + // display one marker, + // but hide the other + displayMarker(markerData.MovingEnd); + otherMarker.Visibility(Visibility::Collapsed); + } + } + else + { + // hide selection markers + SelectionStartMarker().Visibility(Visibility::Collapsed); + SelectionEndMarker().Visibility(Visibility::Collapsed); + } + } + } + + til::point TermControl::_toPosInDips(const Core::Point terminalCellPos) + { + const til::point terminalPos{ terminalCellPos }; + const til::size marginsInDips{ til::math::rounding, GetPadding().Left, GetPadding().Top }; + const til::size fontSize{ til::math::rounding, _core.FontSize() }; + const til::point posInPixels{ terminalPos * fontSize }; + const auto scale{ SwapChainPanel().CompositionScaleX() }; + const til::point posInDIPs{ til::math::flooring, posInPixels.x / scale, posInPixels.y / scale }; + return posInDIPs + marginsInDips; + } + void TermControl::_coreFontSizeChanged(const int fontWidth, const int fontHeight, const bool isInitialChange) { + // scale the selection markers to be the size of a cell + auto scaleMarker = [fontWidth, fontHeight, dpiScale{ SwapChainPanel().CompositionScaleX() }](const Windows::UI::Xaml::Shapes::Path& shape) { + // The selection markers were designed to be 5x14 in size, + // so use those dimensions below for the scaling + const auto scaleX = fontWidth / 5.0 / dpiScale; + const auto scaleY = fontHeight / 14.0 / dpiScale; + + Windows::UI::Xaml::Media::ScaleTransform transform; + transform.ScaleX(scaleX); + transform.ScaleY(scaleY); + shape.RenderTransform(transform); + + // now hide the shape + shape.Visibility(Visibility::Collapsed); + }; + scaleMarker(SelectionStartMarker()); + scaleMarker(SelectionEndMarker()); + // Don't try to inspect the core here. The Core is raising this while // it's holding its write lock. If the handlers calls back to some // method on the TermControl on the same thread, and that _method_ calls diff --git a/src/cascadia/TerminalControl/TermControl.h b/src/cascadia/TerminalControl/TermControl.h index de091b67484..0bde6b53fb0 100644 --- a/src/cascadia/TerminalControl/TermControl.h +++ b/src/cascadia/TerminalControl/TermControl.h @@ -287,6 +287,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _FontInfoHandler(const IInspectable& sender, const FontInfoEventArgs& eventArgs); winrt::fire_and_forget _hoveredHyperlinkChanged(IInspectable sender, IInspectable args); + winrt::fire_and_forget _updateSelectionMarkers(IInspectable sender, Control::UpdateSelectionMarkersEventArgs args); void _coreFontSizeChanged(const int fontWidth, const int fontHeight, @@ -295,6 +296,8 @@ namespace winrt::Microsoft::Terminal::Control::implementation void _coreRaisedNotice(const IInspectable& s, const Control::NoticeEventArgs& args); void _coreWarningBell(const IInspectable& sender, const IInspectable& args); void _coreFoundMatch(const IInspectable& sender, const Control::FoundResultsArgs& args); + + til::point _toPosInDips(const Core::Point terminalCellPos); void _throttledUpdateScrollbar(const ScrollBarUpdate& update); }; } diff --git a/src/cascadia/TerminalControl/TermControl.xaml b/src/cascadia/TerminalControl/TermControl.xaml index dcb24c353b3..96c9c575895 100644 --- a/src/cascadia/TerminalControl/TermControl.xaml +++ b/src/cascadia/TerminalControl/TermControl.xaml @@ -1213,6 +1213,16 @@ + + + + + we're moving end endpoint ("lower") + // false --> we're moving start endpoint ("higher") + return _selection->start == _selection->pivot; +} + +bool Terminal::MovingCursor() const noexcept +{ + // Relevant for keyboard selection: + // true --> the selection is just a "cursor"; we're moving everything together with arrow keys + // false --> otherwise + return _selection->start == _selection->pivot && _selection->pivot == _selection->end; +} + // Method Description: // - updates the selection endpoints based on a direction and expansion mode. Primarily used for keyboard selection. // Arguments: @@ -319,11 +372,9 @@ Terminal::UpdateSelectionParams Terminal::ConvertKeyEventToUpdateSelectionParams void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion mode, ControlKeyStates mods) { // 1. Figure out which endpoint to update - // If we're in mark mode, shift dictates whether you are moving the end or not. - // Otherwise, we're updating an existing selection, so one of the endpoints is the pivot, - // signifying that the other endpoint is the one we want to move. - const auto movingEnd{ _markMode ? mods.IsShiftPressed() : _selection->start == _selection->pivot }; - auto targetPos{ movingEnd ? _selection->end : _selection->start }; + // One of the endpoints is the pivot, + // signifying that the other endpoint is the one we want to move. + auto targetPos{ MovingEnd() ? _selection->end : _selection->start }; // 2. Perform the movement switch (mode) @@ -343,21 +394,17 @@ void Terminal::UpdateSelection(SelectionDirection direction, SelectionExpansion } // 3. Actually modify the selection - // NOTE: targetStart doesn't matter here - if (_markMode) + if (_markMode && !mods.IsShiftPressed()) { - // [Mark Mode] - // - moveSelectionEnd --> just move end (i.e. shift + arrow keys) - // - !moveSelectionEnd --> move all three (i.e. just use arrow keys) + // [Mark Mode] + shift unpressed --> move all three (i.e. just use arrow keys) + _selection->start = targetPos; _selection->end = targetPos; - if (!movingEnd) - { - _selection->start = targetPos; - _selection->pivot = targetPos; - } + _selection->pivot = targetPos; } else { + // [Mark Mode] + shift --> updating a standard selection + // NOTE: targetStart doesn't matter here auto targetStart = false; std::tie(_selection->start, _selection->end) = _PivotSelection(targetPos, targetStart); }