// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "pch.h" #include "TSFInputControl.h" #include "TSFInputControl.g.cpp" #include using namespace winrt::Windows::Foundation; using namespace winrt::Windows::Graphics::Display; using namespace winrt::Windows::UI::Core; using namespace winrt::Windows::UI::Text; using namespace winrt::Windows::UI::Text::Core; using namespace winrt::Windows::UI::Xaml; namespace winrt::Microsoft::Terminal::TerminalControl::implementation { TSFInputControl::TSFInputControl() : _editContext{ nullptr }, _inComposition{ false }, _activeTextStart{ 0 }, _focused{ false }, _currentTerminalCursorPos{ 0, 0 }, _currentCanvasWidth{ 0.0 }, _currentTextBlockHeight{ 0.0 }, _currentTextBounds{ 0, 0, 0, 0 }, _currentControlBounds{ 0, 0, 0, 0 } { InitializeComponent(); // Create a CoreTextEditingContext for since we are acting like a custom edit control auto manager = Core::CoreTextServicesManager::GetForCurrentView(); _editContext = manager.CreateEditContext(); // InputPane is manually shown inside of TermControl. _editContext.InputPaneDisplayPolicy(Core::CoreTextInputPaneDisplayPolicy::Manual); // set the input scope to Text because this control is for any text. _editContext.InputScope(Core::CoreTextInputScope::Text); _textRequestedRevoker = _editContext.TextRequested(winrt::auto_revoke, { this, &TSFInputControl::_textRequestedHandler }); _selectionRequestedRevoker = _editContext.SelectionRequested(winrt::auto_revoke, { this, &TSFInputControl::_selectionRequestedHandler }); _focusRemovedRevoker = _editContext.FocusRemoved(winrt::auto_revoke, { this, &TSFInputControl::_focusRemovedHandler }); _textUpdatingRevoker = _editContext.TextUpdating(winrt::auto_revoke, { this, &TSFInputControl::_textUpdatingHandler }); _selectionUpdatingRevoker = _editContext.SelectionUpdating(winrt::auto_revoke, { this, &TSFInputControl::_selectionUpdatingHandler }); _formatUpdatingRevoker = _editContext.FormatUpdating(winrt::auto_revoke, { this, &TSFInputControl::_formatUpdatingHandler }); _layoutRequestedRevoker = _editContext.LayoutRequested(winrt::auto_revoke, { this, &TSFInputControl::_layoutRequestedHandler }); _compositionStartedRevoker = _editContext.CompositionStarted(winrt::auto_revoke, { this, &TSFInputControl::_compositionStartedHandler }); _compositionCompletedRevoker = _editContext.CompositionCompleted(winrt::auto_revoke, { this, &TSFInputControl::_compositionCompletedHandler }); } // Method Description: // - Prepares this TSFInputControl to be removed from the UI hierarchy. void TSFInputControl::Close() { // Explicitly disconnect the LayoutRequested handler -- it can cause problems during application teardown. // See GH#4159 for more info. // Also disconnect compositionCompleted and textUpdating explicitly. It seems to occasionally cause problems if // a composition is active during application teardown. _layoutRequestedRevoker.revoke(); _compositionCompletedRevoker.revoke(); _textUpdatingRevoker.revoke(); } // Method Description: // - NotifyFocusEnter handler for notifying CoreEditTextContext of focus enter // when TerminalControl receives focus. // Arguments: // - // Return Value: // - void TSFInputControl::NotifyFocusEnter() { if (_editContext != nullptr) { _editContext.NotifyFocusEnter(); _focused = true; } } // Method Description: // - NotifyFocusEnter handler for notifying CoreEditTextContext of focus leaving. // when TerminalControl no longer has focus. // Arguments: // - // Return Value: // - void TSFInputControl::NotifyFocusLeave() { if (_editContext != nullptr) { _editContext.NotifyFocusLeave(); _focused = false; } } // Method Description: // - Clears the input buffer and tells the text server to clear their buffer as well. // Also clears the TextBlock and sets the active text starting point to 0. // Arguments: // - // Return Value: // - void TSFInputControl::ClearBuffer() { if (!_inputBuffer.empty()) { TextBlock().Text(L""); const auto bufLen = ::base::ClampedNumeric(_inputBuffer.length()); _inputBuffer.clear(); _editContext.NotifyFocusLeave(); _editContext.NotifyTextChanged({ 0, bufLen }, 0, { 0, 0 }); _editContext.NotifyFocusEnter(); _activeTextStart = 0; _inComposition = false; } } // Method Description: // - Redraw the canvas if certain dimensions have changed since the last // redraw. This includes the Terminal cursor position, the Canvas width, and the TextBlock height. // Arguments: // - // Return Value: // - void TSFInputControl::TryRedrawCanvas() { if (!_focused) { return; } // Get the cursor position in text buffer position auto cursorArgs = winrt::make_self(); _CurrentCursorPositionHandlers(*this, *cursorArgs); const til::point cursorPos{ gsl::narrow_cast(cursorArgs->CurrentPosition().X), gsl::narrow_cast(cursorArgs->CurrentPosition().Y) }; const double actualCanvasWidth = Canvas().ActualWidth(); const double actualTextBlockHeight = TextBlock().ActualHeight(); if (_currentTerminalCursorPos == cursorPos && _currentCanvasWidth == actualCanvasWidth && _currentTextBlockHeight == actualTextBlockHeight) { return; } _currentTerminalCursorPos = cursorPos; _currentCanvasWidth = actualCanvasWidth; _currentTextBlockHeight = actualTextBlockHeight; _RedrawCanvas(); } // Method Description: // - Redraw the Canvas and update the current Text Bounds and Control Bounds for // the CoreTextEditContext. // Arguments: // - // Return Value: // - void TSFInputControl::_RedrawCanvas() { // Get Font Info as we use this is the pixel size for characters in the display auto fontArgs = winrt::make_self(); _CurrentFontInfoHandlers(*this, *fontArgs); const auto fontWidth = fontArgs->FontSize().Width; const auto fontHeight = fontArgs->FontSize().Height; // Convert text buffer cursor position to client coordinate position within the window COORD clientCursorPos; clientCursorPos.X = ::base::ClampMul(_currentTerminalCursorPos.x(), ::base::ClampedNumeric(fontWidth)); clientCursorPos.Y = ::base::ClampMul(_currentTerminalCursorPos.y(), ::base::ClampedNumeric(fontHeight)); // position textblock to cursor position Canvas().SetLeft(TextBlock(), clientCursorPos.X); Canvas().SetTop(TextBlock(), clientCursorPos.Y); // calculate FontSize in pixels from DPIs const double fontSizePx = (fontHeight * 72) / USER_DEFAULT_SCREEN_DPI; TextBlock().FontSize(fontSizePx); TextBlock().FontFamily(Media::FontFamily(fontArgs->FontFace())); const auto widthToTerminalEnd = _currentCanvasWidth - ::base::ClampedNumeric(clientCursorPos.X); // Make sure that we're setting the MaxWidth to a positive number - a // negative number here will crash us in mysterious ways with a useless // stack trace const auto newMaxWidth = std::max(0.0, widthToTerminalEnd); TextBlock().MaxWidth(newMaxWidth); // Get window in screen coordinates, this is the entire window including tabs const auto windowBounds = CoreWindow::GetForCurrentThread().Bounds(); // Convert from client coordinate to screen coordinate by adding window position COORD screenCursorPos; screenCursorPos.X = ::base::ClampAdd(clientCursorPos.X, ::base::ClampedNumeric(windowBounds.X)); screenCursorPos.Y = ::base::ClampAdd(clientCursorPos.Y, ::base::ClampedNumeric(windowBounds.Y)); // get any offset (margin + tabs, etc..) of the control within the window const auto offsetPoint = this->TransformToVisual(nullptr).TransformPoint(winrt::Windows::Foundation::Point(0, 0)); // add the margin offsets if any screenCursorPos.X = ::base::ClampAdd(screenCursorPos.X, ::base::ClampedNumeric(offsetPoint.X)); screenCursorPos.Y = ::base::ClampAdd(screenCursorPos.Y, ::base::ClampedNumeric(offsetPoint.Y)); // Get scale factor for view const double scaleFactor = DisplayInformation::GetForCurrentView().RawPixelsPerViewPixel(); const auto yOffset = ::base::ClampedNumeric(_currentTextBlockHeight) - fontHeight; const auto textBottom = ::base::ClampedNumeric(screenCursorPos.Y) + yOffset; _currentTextBounds = ScaleRect(Rect(screenCursorPos.X, textBottom, 0, fontHeight), scaleFactor); _currentControlBounds = ScaleRect(Rect(screenCursorPos.X, screenCursorPos.Y, 0, fontHeight), scaleFactor); } // Method Description: // - Handler for LayoutRequested event by CoreEditContext responsible // for returning the current position the IME should be placed // in screen coordinates on the screen. TSFInputControls internal // XAML controls (TextBlock/Canvas) are also positioned and updated. // NOTE: documentation says application should handle this event // Arguments: // - sender: CoreTextEditContext sending the request. // - args: CoreTextLayoutRequestedEventArgs to be updated with position information. // Return Value: // - void TSFInputControl::_layoutRequestedHandler(CoreTextEditContext sender, CoreTextLayoutRequestedEventArgs const& args) { auto request = args.Request(); TryRedrawCanvas(); // Set the text block bounds request.LayoutBounds().TextBounds(_currentTextBounds); // Set the control bounds of the whole control request.LayoutBounds().ControlBounds(_currentControlBounds); } // Method Description: // - Handler for CompositionStarted event by CoreEditContext responsible // for making internal TSFInputControl controls visible. // Arguments: // - sender: CoreTextEditContext sending the request. Not used in method. // - args: CoreTextCompositionStartedEventArgs. Not used in method. // Return Value: // - void TSFInputControl::_compositionStartedHandler(CoreTextEditContext sender, CoreTextCompositionStartedEventArgs const& /*args*/) { _inComposition = true; } // Method Description: // - Handler for CompositionCompleted event by CoreEditContext responsible // for making internal TSFInputControl controls visible. // Arguments: // - sender: CoreTextEditContext sending the request. Not used in method. // - args: CoreTextCompositionCompletedEventArgs. Not used in method. // Return Value: // - void TSFInputControl::_compositionCompletedHandler(CoreTextEditContext sender, CoreTextCompositionCompletedEventArgs const& /*args*/) { _inComposition = false; // only need to do work if the current buffer has text if (!_inputBuffer.empty()) { _SendAndClearText(); } } // Method Description: // - Handler for FocusRemoved event by CoreEditContext responsible // for removing focus for the TSFInputControl control accordingly // when focus was forcibly removed from text input control. // NOTE: Documentation says application should handle this event // Arguments: // - sender: CoreTextEditContext sending the request. Not used in method. // - object: CoreTextCompositionStartedEventArgs. Not used in method. // Return Value: // - void TSFInputControl::_focusRemovedHandler(CoreTextEditContext sender, winrt::Windows::Foundation::IInspectable const& /*object*/) { } // Method Description: // - Handler for TextRequested event by CoreEditContext responsible // for returning the range of text requested. // NOTE: Documentation says application should handle this event // Arguments: // - sender: CoreTextEditContext sending the request. Not used in method. // - args: CoreTextTextRequestedEventArgs to be updated with requested range text. // Return Value: // - void TSFInputControl::_textRequestedHandler(CoreTextEditContext sender, CoreTextTextRequestedEventArgs const& args) { // the range the TSF wants to know about const auto range = args.Request().Range(); try { const auto textEnd = ::base::ClampMin(range.EndCaretPosition, _inputBuffer.length()); const auto length = ::base::ClampSub(textEnd, range.StartCaretPosition); const auto textRequested = _inputBuffer.substr(range.StartCaretPosition, length); args.Request().Text(textRequested); } CATCH_LOG(); } // Method Description: // - Handler for SelectionRequested event by CoreEditContext responsible // for returning the currently selected text. // TSFInputControl currently doesn't allow selection, so nothing happens. // NOTE: Documentation says application should handle this event // Arguments: // - sender: CoreTextEditContext sending the request. Not used in method. // - args: CoreTextSelectionRequestedEventArgs for providing data for the SelectionRequested event. Not used in method. // Return Value: // - void TSFInputControl::_selectionRequestedHandler(CoreTextEditContext sender, CoreTextSelectionRequestedEventArgs const& /*args*/) { } // Method Description: // - Handler for SelectionUpdating event by CoreEditContext responsible // for handling modifications to the range of text currently selected. // TSFInputControl doesn't currently allow selection, so nothing happens. // NOTE: Documentation says application should set its selection range accordingly // Arguments: // - sender: CoreTextEditContext sending the request. Not used in method. // - args: CoreTextSelectionUpdatingEventArgs for providing data for the SelectionUpdating event. Not used in method. // Return Value: // - void TSFInputControl::_selectionUpdatingHandler(CoreTextEditContext sender, CoreTextSelectionUpdatingEventArgs const& /*args*/) { } // Method Description: // - Handler for TextUpdating event by CoreEditContext responsible // for handling text updates. // Arguments: // - sender: CoreTextEditContext sending the request. Not used in method. // - args: CoreTextTextUpdatingEventArgs contains new text to update buffer with. // Return Value: // - void TSFInputControl::_textUpdatingHandler(CoreTextEditContext sender, CoreTextTextUpdatingEventArgs const& args) { const auto incomingText = args.Text(); const auto range = args.Range(); try { // When a user deletes the last character in their current composition, some machines // will fire a CompositionCompleted before firing a TextUpdating event that deletes the last character. // The TextUpdating will have a lower StartCaretPosition, so in this scenario, _activeTextStart // needs to update to be the StartCaretPosition. // A known issue related to this behavior is that the last character that's deleted from a composition // will get sent to the terminal before we receive the TextUpdate to delete the character. // See GH #5054. _activeTextStart = ::base::ClampMin(_activeTextStart, ::base::ClampedNumeric(range.StartCaretPosition)); _inputBuffer = _inputBuffer.replace( range.StartCaretPosition, ::base::ClampSub(range.EndCaretPosition, range.StartCaretPosition), incomingText); // Emojis/Kaomojis/Symbols chosen through the IME without starting composition // will be sent straight through to the terminal. if (!_inComposition) { _SendAndClearText(); } else { Canvas().Visibility(Visibility::Visible); const auto text = _inputBuffer.substr(_activeTextStart); TextBlock().Text(text); } // Notify the TSF that the update succeeded args.Result(CoreTextTextUpdatingResult::Succeeded); } catch (...) { LOG_CAUGHT_EXCEPTION(); // indicate updating failed. args.Result(CoreTextTextUpdatingResult::Failed); } } // Method Description: // - Send the portion of the textBuffer starting at _activeTextStart to the end of the buffer. // Then clear the TextBlock and hide it until the next time text is received. // Arguments: // - // Return Value: // - void TSFInputControl::_SendAndClearText() { const auto text = _inputBuffer.substr(_activeTextStart); _compositionCompletedHandlers(text); _activeTextStart = _inputBuffer.length(); TextBlock().Text(L""); // hide the controls until text input starts again Canvas().Visibility(Visibility::Collapsed); } // Method Description: // - Handler for FormatUpdating event by CoreEditContext responsible // for handling different format updates for a particular range of text. // TSFInputControl doesn't do anything with this event. // Arguments: // - sender: CoreTextEditContext sending the request. Not used in method. // - args: CoreTextFormatUpdatingEventArgs Provides data for the FormatUpdating event. Not used in method. // Return Value: // - void TSFInputControl::_formatUpdatingHandler(CoreTextEditContext sender, CoreTextFormatUpdatingEventArgs const& /*args*/) { } DEFINE_EVENT(TSFInputControl, CompositionCompleted, _compositionCompletedHandlers, TerminalControl::CompositionCompletedEventArgs); }