diff --git a/README.md b/README.md index ad824fb78..c79e866b9 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ This repository hosts samples for the [Windows App SDK](https://github.com/micro - [Push Notifications](Samples/Notifications/Push): This is a sample app that showcases Push Notifications. #### User Interface and Input +- [CustomEditControl](Samples/CustomEditControl): This sample demonstrates how to create a text edit control with customized UI and behavior. - [Windowing](Samples/Windowing): This sample demonstrates how to manage app windows using the Windowing APIs. - [Windows Input and Composition Gallery](https://github.com/microsoft/WindowsCompositionSamples): This collection of samples showcases Microsoft.UI.Composition and Microsoft.UI.Input APIs. - [XAML Controls Gallery](https://github.com/microsoft/Xaml-Controls-Gallery/tree/winui3): This is a sample app that showcases all of the WinUI 3 controls in action. diff --git a/Samples/CustomEditControl/README.md b/Samples/CustomEditControl/README.md new file mode 100644 index 000000000..01a308e50 --- /dev/null +++ b/Samples/CustomEditControl/README.md @@ -0,0 +1,94 @@ +--- +page_type: sample +languages: +- csharp +- cppwinrt +- cpp +products: +- windows +- windows-app-sdk +name: "Custom Edit Control" +urlFragment: CustomEditControl +description: "Shows how to use the CoreTextEditContext class to create a rudimentary text control." +extendedZipContent: +- path: LICENSE + target: LICENSE +--- + +# Custom Edit Control sample + +Shows how to use the CoreTextEditContext class in the Windows.UI.Text.Core namespace +to create a rudimentary text control. Note that this text control is not complete; +it glosses over many details that would be necessary for a complete text edit control. + +The focus of the sample is in CustomEditControl to show how to implement and manage a text control. +There is much more functionality built in to the CoreTextEditContext that is not covered in this sample. + +This sample demonstrates the following: + +* Managing the text and current selection of a custom edit control. + This sample uses a simple string to record the text. +* Rendering the text and current selection in the custom edit control. + To illustrate that the app is completely responsible for the visual + presentation of the control, this sample takes the unusual decision + to use a globe to represent the caret. +* Manually setting focus to and removing focus from the control + and the CoreTextEditContext based on application-defined criteria. + The CoreTextEditContext processes text input when it has focus. +* Setting the CoreTextEditContext.InputPaneDisplayPolicy to Manual + and manually showing the software keyboard when the custom edit control + gains focus and hiding it when the custom edit control loses focus. +* Responding to system events that request information about the + custom edit control or request changes to the text and selection of + the custom edit control. +* Responding to layout information requests so that the IME candidate window + can be positioned properly. +* Changing the selection and/or moving the caret when the user presses + an arrow key, and deleting text when the user presses the Backspace key. + +**Instructions on using this sample** + +* Click or tap on the custom edit control to give it focus, + and click or tap outside the custom edit control to remove focus. +* Observe that the Input Pane appears (if applicable) + when the custom edit control gains focus, + and it disappears when the custom edit control loses focus. +* Use the arrow keys to move the caret (shown as a globe). +* Hold the shift key when pressing the arrow keys to adjust + the selection. +* Use the Backspace key to delete text. +* To demonstrate support for IME candidates: + * Install an IME by going to Settings, Time and Language, + Region and language. Click "Add a language" and select + Chinese (Simplified) "中文(中华人民共和国)". + * Set your input language to Chinese by using the language + selector in the bottom right corner of the taskbar (on Desktop) + or by swiping the space bar on the software keyboard (on Mobile). + * Put focus on the custom edit control and start typing. + IME candidate suggestions will appear as you type. + +**Features missing from this sample** + +* This sample does not properly handle surrogate pairs + or grapheme clusters. +* This sample does not support common keyboard shortcuts + such as Home and End, nor does it support shortcuts such + as Ctrl+V to paste. +* This sample does not show a context menu if the user right-clicks + or performs a press-and-hold gesture. +* This sample does not support using the mouse or touch to + move the caret or adjust the selection. + +## Prerequisites + +* See [System requirements for Windows app development](https://docs.microsoft.com/windows/apps/windows-app-sdk/system-requirements). +* Make sure that your development environment is set up correctly—see [Install tools for developing apps for Windows 10 and Windows 11](https://docs.microsoft.com/windows/apps/windows-app-sdk/set-up-your-development-environment). + +## Building and running any of the samples + +* Open the solution file (`.sln`) from the subfolder of your preferred sample in Visual Studio. +* From Visual Studio, either **Start Without Debugging** (Ctrl+F5) or **Start Debugging** (F5). + +## Related Links + +- [CoreTextEditContext](https://msdn.microsoft.com/library/windows/apps/windows.ui.text.core.coretexteditcontext.aspx) diff --git a/Samples/CustomEditControl/cpp-winui/App.idl b/Samples/CustomEditControl/cpp-winui/App.idl new file mode 100644 index 000000000..8220787fc --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/App.idl @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CustomEditControlCpp +{ +} diff --git a/Samples/CustomEditControl/cpp-winui/App.xaml b/Samples/CustomEditControl/cpp-winui/App.xaml new file mode 100644 index 000000000..7988e33c9 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/App.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/Samples/CustomEditControl/cpp-winui/App.xaml.cpp b/Samples/CustomEditControl/cpp-winui/App.xaml.cpp new file mode 100644 index 000000000..28bb4e400 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/App.xaml.cpp @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" + +#include "App.xaml.h" +#include "MainWindow.xaml.h" + +using namespace winrt; +using namespace Windows::Foundation; +using namespace Microsoft::UI::Xaml; +using namespace Microsoft::UI::Xaml::Controls; +using namespace Microsoft::UI::Xaml::Navigation; +using namespace CustomEditControlCpp; +using namespace CustomEditControlCpp::implementation; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +/// +/// Initializes the singleton application object. This is the first line of authored code +/// executed, and as such is the logical equivalent of main() or WinMain(). +/// +App::App() +{ + InitializeComponent(); + +#if defined _DEBUG && !defined DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION + UnhandledException([this](IInspectable const&, UnhandledExceptionEventArgs const& e) + { + if (IsDebuggerPresent()) + { + auto errorMessage = e.Message(); + __debugbreak(); + } + }); +#endif +} + +/// +/// Invoked when the application is launched normally by the end user. Other entry points +/// will be used such as when the application is launched to open a specific file. +/// +/// Details about the launch request and process. +void App::OnLaunched(LaunchActivatedEventArgs const&) +{ + window = make(); + window.Activate(); +} diff --git a/Samples/CustomEditControl/cpp-winui/App.xaml.h b/Samples/CustomEditControl/cpp-winui/App.xaml.h new file mode 100644 index 000000000..a6c20b1d4 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/App.xaml.h @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "App.xaml.g.h" + +namespace winrt::CustomEditControlCpp::implementation +{ + struct App : AppT + { + App(); + + void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&); + + private: + winrt::Microsoft::UI::Xaml::Window window{ nullptr }; + }; +} diff --git a/Samples/CustomEditControl/cpp-winui/Assets/LockScreenLogo.scale-200.png b/Samples/CustomEditControl/cpp-winui/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 000000000..7440f0d4b Binary files /dev/null and b/Samples/CustomEditControl/cpp-winui/Assets/LockScreenLogo.scale-200.png differ diff --git a/Samples/CustomEditControl/cpp-winui/Assets/SplashScreen.scale-200.png b/Samples/CustomEditControl/cpp-winui/Assets/SplashScreen.scale-200.png new file mode 100644 index 000000000..32f486a86 Binary files /dev/null and b/Samples/CustomEditControl/cpp-winui/Assets/SplashScreen.scale-200.png differ diff --git a/Samples/CustomEditControl/cpp-winui/Assets/Square150x150Logo.scale-200.png b/Samples/CustomEditControl/cpp-winui/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 000000000..53ee3777e Binary files /dev/null and b/Samples/CustomEditControl/cpp-winui/Assets/Square150x150Logo.scale-200.png differ diff --git a/Samples/CustomEditControl/cpp-winui/Assets/Square44x44Logo.scale-200.png b/Samples/CustomEditControl/cpp-winui/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 000000000..f713bba67 Binary files /dev/null and b/Samples/CustomEditControl/cpp-winui/Assets/Square44x44Logo.scale-200.png differ diff --git a/Samples/CustomEditControl/cpp-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Samples/CustomEditControl/cpp-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 000000000..dc9f5bea0 Binary files /dev/null and b/Samples/CustomEditControl/cpp-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/Samples/CustomEditControl/cpp-winui/Assets/StoreLogo.png b/Samples/CustomEditControl/cpp-winui/Assets/StoreLogo.png new file mode 100644 index 000000000..a4586f26b Binary files /dev/null and b/Samples/CustomEditControl/cpp-winui/Assets/StoreLogo.png differ diff --git a/Samples/CustomEditControl/cpp-winui/Assets/Wide310x150Logo.scale-200.png b/Samples/CustomEditControl/cpp-winui/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 000000000..8b4a5d0dd Binary files /dev/null and b/Samples/CustomEditControl/cpp-winui/Assets/Wide310x150Logo.scale-200.png differ diff --git a/Samples/CustomEditControl/cpp-winui/CustomEditControl.idl b/Samples/CustomEditControl/cpp-winui/CustomEditControl.idl new file mode 100644 index 000000000..b9f4cb44f --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/CustomEditControl.idl @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CustomEditControlCpp +{ + [default_interface] + runtimeclass CustomEditControl : Microsoft.UI.Xaml.Controls.UserControl + { + CustomEditControl(); + void SetAppWindow(Microsoft.UI.WindowId windowId); + void SetInternalFocus(); + void RemoveInternalFocus(); + } +} diff --git a/Samples/CustomEditControl/cpp-winui/CustomEditControl.xaml b/Samples/CustomEditControl/cpp-winui/CustomEditControl.xaml new file mode 100644 index 000000000..9cf87557a --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/CustomEditControl.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + Full text: + Selection start index: + Selection end index: + + + diff --git a/Samples/CustomEditControl/cpp-winui/CustomEditControl.xaml.cpp b/Samples/CustomEditControl/cpp-winui/CustomEditControl.xaml.cpp new file mode 100644 index 000000000..334dc580d --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/CustomEditControl.xaml.cpp @@ -0,0 +1,574 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "CustomEditControl.xaml.h" +#if __has_include("CustomEditControl.g.cpp") +#include "CustomEditControl.g.cpp" +#endif + +#include "InputPaneInterop.h" + +namespace winrt +{ + using namespace ::winrt::Microsoft::UI::Input; + using namespace ::winrt::Microsoft::UI::Windowing; + using namespace ::winrt::Microsoft::UI::Xaml; + using namespace ::winrt::Microsoft::UI::Xaml::Documents; + using namespace ::winrt::Microsoft::UI::Xaml::Input; + using namespace ::winrt::Microsoft::UI::Xaml::Media; + using namespace ::winrt::Windows::Foundation; + using namespace ::winrt::Windows::Graphics::Display; + using namespace ::winrt::Windows::System; + using namespace ::winrt::Windows::UI; + using namespace ::winrt::Windows::UI::Core; + using namespace ::winrt::Windows::UI::Text::Core; + using namespace ::winrt::Windows::UI::ViewManagement; +} + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +// https://docs.microsoft.com/en-us/windows/apps/desktop/modernize/advanced-scenarios-xaml-islands-cpp + +namespace winrt::CustomEditControlCpp::implementation +{ + CustomEditControl::CustomEditControl() + { + InitializeComponent(); + + // The CoreTextEditContext processes text input, but other keys are + // the app's responsibility. + // Create a CoreTextEditContext for our custom edit control. + winrt::CoreTextServicesManager manager = winrt::CoreTextServicesManager::GetForCurrentView(); + _editContext = manager.CreateEditContext(); + + // For demonstration purposes, this sample sets the Input Pane display policy to Manual + // so that it can manually show the software keyboard when the control gains focus and + // dismiss it when the control loses focus. If you leave the policy as Automatic, then + // the system will hide and show the Input Pane for you. Note that on Desktop, you will + // need to implement the UIA text pattern to get expected automatic behavior. + _editContext.InputPaneDisplayPolicy(winrt::CoreTextInputPaneDisplayPolicy::Manual); + + // Set the input scope to Text because this text box is for any text. + // This also informs software keyboards to show their regular + // text entry layout. There are many other input scopes and each will + // inform a keyboard layout and text behavior. + _editContext.InputScope(winrt::CoreTextInputScope::Text); + + // The system raises this event to request a specific range of text. + _tokenTextRequested = _editContext.TextRequested({ this, &CustomEditControl::EditContext_TextRequested }); + + // The system raises this event to request the current selection. + _tokenSelectionRequested = _editContext.SelectionRequested({ this, &CustomEditControl::EditContext_SelectionRequested }); + + // The system raises this event when it wants the edit control to remove focus. + _tokenFocusRemoved = _editContext.FocusRemoved({ this, &CustomEditControl::EditContext_FocusRemoved }); + + // The system raises this event to update text in the edit control. + _tokenTextUpdating = _editContext.TextUpdating({ this, &CustomEditControl::EditContext_TextUpdating }); + + // The system raises this event to change the selection in the edit control. + _tokenSelectionUpdating = _editContext.SelectionUpdating({ this, &CustomEditControl::EditContext_SelectionUpdating }); + + // The system raises this event when it wants the edit control + // to apply formatting on a range of text. + _tokenFormatUpdating = _editContext.FormatUpdating({ this, &CustomEditControl::EditContext_FormatUpdating }); + + // The system raises this event to request layout information. + // This is used to help choose a position for the IME candidate window. + _tokenLayoutRequested = _editContext.LayoutRequested({ this, &CustomEditControl::EditContext_LayoutRequested }); + + // The system raises this event to notify the edit control + // that the string composition has started. + _tokenCompositionStarted = _editContext.CompositionStarted({ this, &CustomEditControl::EditContext_CompositionStarted }); + + // The system raises this event to notify the edit control + // that the string composition is finished. + _tokenCompositionCompleted = _editContext.CompositionCompleted({ this, &CustomEditControl::EditContext_CompositionCompleted }); + + // The system raises this event when the NotifyFocusLeave operation has + // completed. Our sample does not use this event. + // _editContext.NotifyFocusLeaveCompleted += EditContext_NotifyFocusLeaveCompleted; + + // Set our initial UI. + UpdateTextUI(); + UpdateFocusUI(); + } + + CustomEditControl::~CustomEditControl() + { + RemoveInternalFocus(); + } + + static winrt::Rect GetElementRect(winrt::FrameworkElement element) + { + winrt::GeneralTransform transform = element.TransformToVisual(nullptr); + winrt::Point point = transform.TransformPoint(winrt::Point()); + return winrt::Rect(point, winrt::Size((float)element.ActualWidth(), (float)element.ActualHeight())); + } + +#pragma region Focus_management + + void CustomEditControl::OnGotFocus(winrt::RoutedEventArgs args) + { + SetInternalFocus(); + } + + void CustomEditControl::OnLostFocus(winrt::RoutedEventArgs args) + { + RemoveInternalFocus(); + } + + void CustomEditControl::SetAppWindow(winrt::Microsoft::UI::WindowId windowId) + { + _appWindowId = windowId; + + HWND hwnd = GetWindowFromWindowId(windowId); + if (hwnd) + { + // Get the Input Pane so we can programmatically hide and show it. + _inputPane = wil::capture_interop(&IInputPaneInterop::GetForWindow, hwnd); + } + } + + void CustomEditControl::SetInternalFocus() + { + if (!_internalFocus) + { + // Update internal notion of focus. + _internalFocus = true; + + // Update the UI. + UpdateTextUI(); + UpdateFocusUI(); + + // Notify the CoreTextEditContext that the edit context has focus, + // so it should start processing text input. + _editContext.NotifyFocusEnter(); + } + + // Ask the software keyboard to show. The system will ultimately decide if it will show. + // For example, it will not show if there is a keyboard attached. + _inputPane.TryShow(); + } + + void CustomEditControl::RemoveInternalFocus() + { + if (_internalFocus) + { + //Notify the system that this edit context is no longer in focus + _editContext.NotifyFocusLeave(); + + RemoveInternalFocusWorker(); + } + } + + void CustomEditControl::RemoveInternalFocusWorker() + { + // Update the internal notion of focus + _internalFocus = false; + + // Ask the software keyboard to dismiss. + _inputPane.TryHide(); + + // Update our UI. + UpdateTextUI(); + UpdateFocusUI(); + } + + void CustomEditControl::EditContext_FocusRemoved(CoreTextEditContext sender, winrt::IInspectable args) + { + RemoveInternalFocusWorker(); + } + +#pragma endregion Focus_management + +#pragma region Text_management + + // Replace the text in the specified range. + void CustomEditControl::ReplaceText(winrt::CoreTextRange modifiedRange, winrt::hstring text) + { + // Modify the internal text store. + _text = _text.substr(0, modifiedRange.StartCaretPosition) + + text + + _text.substr(modifiedRange.EndCaretPosition); + + // Move the caret to the end of the replacement text. + _selection.StartCaretPosition = modifiedRange.StartCaretPosition + text.size(); + _selection.EndCaretPosition = _selection.StartCaretPosition; + + // Update the selection of the edit context. There is no need to notify the system + // of the selection change because we are going to call NotifyTextChanged soon. + SetSelection(_selection); + + // Let the CoreTextEditContext know what changed. + _editContext.NotifyTextChanged(modifiedRange, text.size(), _selection); + } + + bool CustomEditControl::HasSelection() + { + return _selection.StartCaretPosition != _selection.EndCaretPosition; + } + + // Change the selection without notifying CoreTextEditContext of the new selection. + void CustomEditControl::SetSelection(winrt::CoreTextRange selection) + { + // Modify the internal selection. + _selection = selection; + + // Update the UI to show the new selection. + UpdateTextUI(); + } + + // Change the selection and notify CoreTextEditContext of the new selection. + void CustomEditControl::SetSelectionAndNotify(winrt::CoreTextRange selection) + { + SetSelection(selection); + _editContext.NotifySelectionChanged(_selection); + } + + // Return the specified range of text. Note that the system may ask for more text + // than exists in the text buffer. + void CustomEditControl::EditContext_TextRequested(winrt::CoreTextEditContext sender, winrt::CoreTextTextRequestedEventArgs args) + { + winrt::CoreTextTextRequest request = args.Request(); + request.Text(winrt::hstring(_text.substr( + request.Range().StartCaretPosition, + min(request.Range().EndCaretPosition, static_cast(_text.size())) - request.Range().StartCaretPosition))); + } + + // Return the current selection. + void CustomEditControl::EditContext_SelectionRequested(winrt::CoreTextEditContext sender, winrt::CoreTextSelectionRequestedEventArgs args) + { + winrt::CoreTextSelectionRequest request = args.Request(); + request.Selection(_selection); + } + + void CustomEditControl::EditContext_TextUpdating(winrt::CoreTextEditContext sender, winrt::CoreTextTextUpdatingEventArgs args) + { + winrt::CoreTextRange range = args.Range(); + winrt::hstring newText = args.Text(); + winrt::CoreTextRange newSelection = args.NewSelection(); + + // Modify the internal text store. + _text = _text.substr(0, range.StartCaretPosition) + + newText + + _text.substr(min(static_cast(_text.size()), range.EndCaretPosition)); + + // You can set the proper font or direction for the updated text based on the language by checking + // args.InputLanguage. We will not do that in this sample. + + // Modify the current selection. + newSelection.EndCaretPosition = newSelection.StartCaretPosition; + + // Update the selection of the edit context. There is no need to notify the system + // because the system itself changed the selection. + SetSelection(newSelection); + } + + void CustomEditControl::EditContext_SelectionUpdating(winrt::CoreTextEditContext sender, winrt::CoreTextSelectionUpdatingEventArgs args) + { + // Set the new selection to the value specified by the system. + CoreTextRange range = args.Selection(); + + // Update the selection of the edit context. There is no need to notify the system + // because the system itself changed the selection. + SetSelection(range); + } + +#pragma endregion Text_management + +#pragma region Formatting_and_layout + + void CustomEditControl::EditContext_FormatUpdating(winrt::CoreTextEditContext sender, winrt::CoreTextFormatUpdatingEventArgs args) + { + // The following code specifies how you would apply any formatting to the specified range of text + // For this sample, we do not make any changes to the format. + + // Apply text color if specified. + // A null value indicates that the default should be used. + if (args.TextColor() != nullptr) + { + // InternalSetTextColor(args.Range(), args.TextColor().Value()); + } + else + { + // InternalSetDefaultTextColor(args.Range()); + } + + // Apply background color if specified. + // A null value indicates that the default should be used. + if (args.BackgroundColor() != nullptr) + { + // InternalSetBackgroundColor(args.Range(), args.BackgroundColor().Value()); + } + else + { + // InternalSetDefaultBackgroundColor(args.Range()); + } + + // Apply underline if specified. + // A null value indicates that the default should be used. + if (args.UnderlineType() != nullptr) + { + // TextDecoration underline = new TextDecoration(args.Range(), args.UnderlineType.Value(), args.UnderlineColor.Value()); + + // InternalAddTextDecoration(underline); + } + else + { + // InternalRemoveTextDecoration(args.Range()); + } + } + + static Rect ScaleRect(Rect rect, float scale) + { + rect.X *= scale; + rect.Y *= scale; + rect.Width *= scale; + rect.Height *= scale; + return rect; + } + + void CustomEditControl::EditContext_LayoutRequested(winrt::CoreTextEditContext sender, winrt::CoreTextLayoutRequestedEventArgs args) + { + winrt::CoreTextLayoutRequest request = args.Request(); + + // Get the screen coordinates of the entire control and the selected text. + // This information is used to position the IME candidate window. + + // First, get the coordinates of the edit control and the selection + // relative to the Window. + + Rect contentRect = GetElementRect(ContentPanel()); + Rect selectionRect = GetElementRect(SelectionText()); + + // Next, convert to screen coordinates in view pixels. + winrt::AppWindow appWindow = winrt::AppWindow::GetFromWindowId(_appWindowId); + contentRect.X += appWindow.Position().X; + contentRect.Y += appWindow.Position().Y; + selectionRect.X += appWindow.Position().X; + selectionRect.Y += appWindow.Position().Y; + + // Finally, scale up to raw pixels. + float scaleFactor = static_cast(XamlRoot().RasterizationScale()); + + contentRect = ScaleRect(contentRect, scaleFactor); + selectionRect = ScaleRect(selectionRect, scaleFactor); + + // This is the bounds of the selection. + // Note: If you return bounds with 0 width and 0 height, candidates will not appear while typing. + request.LayoutBounds().TextBounds(selectionRect); + + // This is the bounds of the whole control + request.LayoutBounds().ControlBounds(contentRect); + } + +#pragma endregion Formatting_and_layout + +#pragma region Input + + // This indicates that an IME has started composition. If there is no handler for this event, + // then composition will not start. + void CustomEditControl::EditContext_CompositionStarted(winrt::CoreTextEditContext sender, winrt::CoreTextCompositionStartedEventArgs args) + { + } + + void CustomEditControl::EditContext_CompositionCompleted(winrt::CoreTextEditContext sender, winrt::CoreTextCompositionCompletedEventArgs args) + { + } + + void CustomEditControl::OnCharacterReceived(winrt::Microsoft::UI::Xaml::Input::CharacterReceivedRoutedEventArgs args) + { + // Do not process keyboard input if the custom edit control does not + // have focus. + if (!_internalFocus) + { + return; + } + + if (isprint(args.Character())) + { + ReplaceText(_selection, winrt::to_hstring(args.Character())); + UpdateTextUI(); + } + } + + void CustomEditControl::OnKeyDown(winrt::KeyRoutedEventArgs args) + { + // Do not process keyboard input if the custom edit control does not + // have focus. + if (!_internalFocus) + { + return; + } + + // This holds the range we intend to operate on, or which we intend + // to become the new selection. Start with the current selection. + winrt::CoreTextRange range = _selection; + + // For the purpose of this sample, we will support only the left and right + // arrow keys and the backspace key. A more complete text edit control + // would also handle keys like Home, End, and Delete, as well as + // hotkeys like Ctrl+V to paste. + // + // Note that this sample does not properly handle surrogate pairs + // nor does it handle grapheme clusters. + + switch (args.Key()) + { + // Backspace + case winrt::VirtualKey::Back: + // If there is a selection, then delete the selection. + if (HasSelection()) + { + // Set the text in the selection to nothing. + ReplaceText(range, L""); + } + else + { + // Delete the character to the left of the caret, if one exists, + // by creating a range that encloses the character to the left + // of the caret, and setting the contents of that range to nothing. + range.StartCaretPosition = max(0, range.StartCaretPosition - 1); + ReplaceText(range, L""); + } + break; + + // Left arrow + case winrt::VirtualKey::Left: + + // If the shift key is down, then adjust the size of the selection. + if (WI_IsFlagSet(winrt::InputKeyboardSource::GetKeyStateForCurrentThread(winrt::VirtualKey::Shift), winrt::CoreVirtualKeyStates::Down)) + { + // If this is the start of a selection, then remember which edge we are adjusting. + if (!HasSelection()) + { + _extendingLeft = true; + } + + // Adjust the selection and notify CoreTextEditContext. + AdjustSelectionEndpoint(-1); + } + else + { + // The shift key is not down. If there was a selection, then snap the + // caret at the left edge of the selection. + if (HasSelection()) + { + range.EndCaretPosition = range.StartCaretPosition; + SetSelectionAndNotify(range); + } + else + { + // There was no selection. Move the caret left one code unit if possible. + range.StartCaretPosition = max(0, range.StartCaretPosition - 1); + range.EndCaretPosition = range.StartCaretPosition; + SetSelectionAndNotify(range); + } + } + break; + + // Right arrow + case winrt::VirtualKey::Right: + + // If the shift key is down, then adjust the size of the selection. + if (WI_IsFlagSet(winrt::InputKeyboardSource::GetKeyStateForCurrentThread(winrt::VirtualKey::Shift), winrt::CoreVirtualKeyStates::Down)) + { + // If this is the start of a selection, then remember which edge we are adjusting. + if (!HasSelection()) + { + _extendingLeft = false; + } + + // Adjust the selection and notify CoreTextEditContext. + AdjustSelectionEndpoint(1); + } + else + { + // The shift key is not down. If there was a selection, then snap the + // caret at the right edge of the selection. + if (HasSelection()) + { + range.StartCaretPosition = range.EndCaretPosition; + SetSelectionAndNotify(range); + } + else + { + // There was no selection. Move the caret right one code unit if possible. + range.StartCaretPosition = min(static_cast(_text.length()), range.StartCaretPosition + 1); + range.EndCaretPosition = range.StartCaretPosition; + SetSelectionAndNotify(range); + } + } + break; + } + } + + // Adjust the active endpoint of the selection in the specified direction. + void CustomEditControl::AdjustSelectionEndpoint(int direction) + { + CoreTextRange range = _selection; + if (_extendingLeft) + { + range.StartCaretPosition = max(0, range.StartCaretPosition + direction); + } + else + { + range.EndCaretPosition = min(static_cast(_text.length()), range.EndCaretPosition + direction); + } + + SetSelectionAndNotify(range); + } + +#pragma endregion Input + +#pragma region UI + + // Helper function to put a zero-width non-breaking space at the end of a string. + // This prevents TextBlock from trimming trailing spaces. + static winrt::hstring PreserveTrailingSpaces(winrt::hstring s) + { + return s + L"\ufeff"; + } + + void CustomEditControl::UpdateFocusUI() + { + BorderPanel().BorderBrush(_internalFocus ? winrt::SolidColorBrush(winrt::Colors::Green()) : nullptr); + } + + void CustomEditControl::UpdateTextUI() + { + // The raw materials we have are a string (_text) and information about + // where the caret/selection is (_selection). We can render the control + // any way we like. + + BeforeSelectionText().Text(PreserveTrailingSpaces(winrt::hstring(_text.substr(0, _selection.StartCaretPosition)))); + + if (HasSelection()) + { + // There is a selection. Draw that. + CaretText().Visibility(winrt::Visibility::Collapsed); + SelectionText().Text(PreserveTrailingSpaces( + winrt::hstring(_text.substr(_selection.StartCaretPosition, _selection.EndCaretPosition - _selection.StartCaretPosition)))); + } + else + { + // There is no selection. Remove it. + SelectionText().Text(L""); + + // Draw the caret if we have focus. + CaretText().Visibility(_internalFocus ? winrt::Visibility::Visible : winrt::Visibility::Collapsed); + } + + AfterSelectionText().Text(PreserveTrailingSpaces(winrt::hstring(_text.substr(_selection.EndCaretPosition)))); + + // Update statistics for demonstration purposes. + FullText().Text(_text); + SelectionStartIndexText().Text(winrt::to_hstring(_selection.StartCaretPosition)); + SelectionEndIndexText().Text(winrt::to_hstring(_selection.EndCaretPosition)); + } + +#pragma endregion UI +} diff --git a/Samples/CustomEditControl/cpp-winui/CustomEditControl.xaml.h b/Samples/CustomEditControl/cpp-winui/CustomEditControl.xaml.h new file mode 100644 index 000000000..c48008b22 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/CustomEditControl.xaml.h @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "winrt/Microsoft.UI.Xaml.h" +#include "winrt/Microsoft.UI.Xaml.Markup.h" +#include "winrt/Microsoft.UI.Xaml.Controls.Primitives.h" +#include "CustomEditControl.g.h" + +using namespace winrt; +using namespace Microsoft::UI::Xaml::Input; + +namespace winrt::CustomEditControlCpp::implementation +{ + struct CustomEditControl : CustomEditControlT + { + CustomEditControl(); + ~CustomEditControl(); + + void OnKeyDown(KeyRoutedEventArgs args); + void OnCharacterReceived(CharacterReceivedRoutedEventArgs args); + + void OnGotFocus(winrt::Microsoft::UI::Xaml::RoutedEventArgs args); + void OnLostFocus(winrt::Microsoft::UI::Xaml::RoutedEventArgs args); + + void SetAppWindow(winrt::Microsoft::UI::WindowId windowId); + + void EditContext_TextRequested( + winrt::Windows::UI::Text::Core::CoreTextEditContext sender, + winrt::Windows::UI::Text::Core::CoreTextTextRequestedEventArgs args); + void EditContext_SelectionRequested( + winrt::Windows::UI::Text::Core::CoreTextEditContext sender, + winrt::Windows::UI::Text::Core::CoreTextSelectionRequestedEventArgs args); + void EditContext_TextUpdating( + winrt::Windows::UI::Text::Core::CoreTextEditContext sender, + winrt::Windows::UI::Text::Core::CoreTextTextUpdatingEventArgs args); + void EditContext_SelectionUpdating( + winrt::Windows::UI::Text::Core::CoreTextEditContext sender, + winrt::Windows::UI::Text::Core::CoreTextSelectionUpdatingEventArgs args); + void EditContext_FocusRemoved( + winrt::Windows::UI::Text::Core::CoreTextEditContext sender, + winrt::Windows::Foundation::IInspectable args); + void EditContext_FormatUpdating( + winrt::Windows::UI::Text::Core::CoreTextEditContext sender, + winrt::Windows::UI::Text::Core::CoreTextFormatUpdatingEventArgs args); + void EditContext_LayoutRequested( + winrt::Windows::UI::Text::Core::CoreTextEditContext sender, + winrt::Windows::UI::Text::Core::CoreTextLayoutRequestedEventArgs args); + void EditContext_CompositionStarted( + winrt::Windows::UI::Text::Core::CoreTextEditContext sender, + winrt::Windows::UI::Text::Core::CoreTextCompositionStartedEventArgs args); + void EditContext_CompositionCompleted( + winrt::Windows::UI::Text::Core::CoreTextEditContext sender, + winrt::Windows::UI::Text::Core::CoreTextCompositionCompletedEventArgs args); + + void UpdateTextUI(); + void UpdateFocusUI(); + + void SetInternalFocus(); + void RemoveInternalFocus(); + void RemoveInternalFocusWorker(); + + void ReplaceText(winrt::Windows::UI::Text::Core::CoreTextRange modifiedRange, winrt::hstring text); + bool HasSelection(); + void SetSelection(winrt::Windows::UI::Text::Core::CoreTextRange selection); + void SetSelectionAndNotify(winrt::Windows::UI::Text::Core::CoreTextRange selection); + + void AdjustSelectionEndpoint(int direction); + + // The _editContext lets us communicate with the input system. + winrt::Windows::UI::Text::Core::CoreTextEditContext _editContext{ nullptr }; + + // We will use a plain text string to represent the + // content of the custom text edit control. + std::wstring _text; + + // If the _selection starts and ends at the same point, + // then it represents the location of the caret (insertion point). + winrt::Windows::UI::Text::Core::CoreTextRange _selection = {}; + + // _internalFocus keeps track of whether our control acts like it has focus. + bool _internalFocus = false; + + // If there is a nonempty selection, then _extendingLeft is true if the user + // is using shift+arrow to adjust the starting point of the selection, + // or false if the user is adjusting the ending point of the selection. + bool _extendingLeft = false; + + // The input pane object indicates the visibility of the on screen keyboard. + // Apps can also ask the keyboard to show or hide. + winrt::Windows::UI::ViewManagement::InputPane _inputPane{ nullptr }; + + // The AppWindow that contains our control. + winrt::Microsoft::UI::WindowId _appWindowId = {}; + + winrt::event_token _tokenTextRequested; + winrt::event_token _tokenSelectionRequested; + winrt::event_token _tokenFocusRemoved; + winrt::event_token _tokenTextUpdating; + winrt::event_token _tokenSelectionUpdating; + winrt::event_token _tokenFormatUpdating; + winrt::event_token _tokenLayoutRequested; + winrt::event_token _tokenCompositionStarted; + winrt::event_token _tokenCompositionCompleted; + winrt::event_token _tokenNotifyFocusLeaveCompleted; + }; +} + +namespace winrt::CustomEditControlCpp::factory_implementation +{ + struct CustomEditControl : CustomEditControlT + { + }; +} diff --git a/Samples/CustomEditControl/cpp-winui/CustomEditControlCpp.sln b/Samples/CustomEditControl/cpp-winui/CustomEditControlCpp.sln new file mode 100644 index 000000000..dcd18c0a6 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/CustomEditControlCpp.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.32002.261 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CustomEditControlCpp", "CustomEditControlCpp.vcxproj", "{4F349AF3-4A2D-439C-BB5E-5E899BD234CA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|arm64 = Debug|arm64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|arm64 = Release|arm64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Debug|arm64.ActiveCfg = Debug|arm64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Debug|arm64.Build.0 = Debug|arm64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Debug|arm64.Deploy.0 = Debug|arm64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Debug|x64.ActiveCfg = Debug|x64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Debug|x64.Build.0 = Debug|x64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Debug|x64.Deploy.0 = Debug|x64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Debug|x86.ActiveCfg = Debug|Win32 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Debug|x86.Build.0 = Debug|Win32 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Debug|x86.Deploy.0 = Debug|Win32 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Release|arm64.ActiveCfg = Release|arm64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Release|arm64.Build.0 = Release|arm64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Release|arm64.Deploy.0 = Release|arm64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Release|x64.ActiveCfg = Release|x64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Release|x64.Build.0 = Release|x64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Release|x64.Deploy.0 = Release|x64 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Release|x86.ActiveCfg = Release|Win32 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Release|x86.Build.0 = Release|Win32 + {4F349AF3-4A2D-439C-BB5E-5E899BD234CA}.Release|x86.Deploy.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D96337A7-121D-449D-B033-DEBE189016BC} + EndGlobalSection +EndGlobal diff --git a/Samples/CustomEditControl/cpp-winui/CustomEditControlCpp.vcxproj b/Samples/CustomEditControl/cpp-winui/CustomEditControlCpp.vcxproj new file mode 100644 index 000000000..72d9b3858 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/CustomEditControlCpp.vcxproj @@ -0,0 +1,191 @@ + + + + + + + true + true + true + {4f349af3-4a2d-439c-bb5e-5e899bd234ca} + CustomEditControlCpp + CustomEditControlCpp + + $(RootNamespace) + en-US + 16.0 + false + true + Windows Store + 10.0 + 10.0 + 10.0.17763.0 + true + true + + + + + Debug + Win32 + + + Debug + x64 + + + Debug + arm64 + + + Release + Win32 + + + Release + x64 + + + Release + arm64 + + + + Application + v143 + Unicode + true + + + true + true + + + false + true + false + + + + + + + + + + + Use + pch.h + $(IntDir)pch.pch + Level4 + %(AdditionalOptions) /bigobj + + + + + _DEBUG;%(PreprocessorDefinitions) + + + + + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + CustomEditControl.xaml + Code + + + + App.xaml + + + MainWindow.xaml + + + + + + Designer + + + + + + CustomEditControl.xaml + Code + + + Create + + + App.xaml + + + MainWindow.xaml + + + + + + Code + App.xaml + + + CustomEditControl.xaml + Code + + + Code + MainWindow.xaml + + + + + + + + + + + + + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + \ No newline at end of file diff --git a/Samples/CustomEditControl/cpp-winui/CustomEditControlCpp.vcxproj.filters b/Samples/CustomEditControl/cpp-winui/CustomEditControlCpp.vcxproj.filters new file mode 100644 index 000000000..2f8eb131d --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/CustomEditControlCpp.vcxproj.filters @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + Assets + + + + + {4f349af3-4a2d-439c-bb5e-5e899bd234ca} + + + + + + + + + + + + \ No newline at end of file diff --git a/Samples/CustomEditControl/cpp-winui/MainWindow.idl b/Samples/CustomEditControl/cpp-winui/MainWindow.idl new file mode 100644 index 000000000..0ac098425 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/MainWindow.idl @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace CustomEditControlCpp +{ + [default_interface] + runtimeclass MainWindow : Microsoft.UI.Xaml.Window + { + MainWindow(); + } +} diff --git a/Samples/CustomEditControl/cpp-winui/MainWindow.xaml b/Samples/CustomEditControl/cpp-winui/MainWindow.xaml new file mode 100644 index 000000000..0bb594581 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/MainWindow.xaml @@ -0,0 +1,30 @@ + + + + + + + + + Text input + + + When the custom text control has focus, it draws with a green border. + The caret is represented by a globe. + See the README for this sample for further instructions. + + + + + + + diff --git a/Samples/CustomEditControl/cpp-winui/MainWindow.xaml.cpp b/Samples/CustomEditControl/cpp-winui/MainWindow.xaml.cpp new file mode 100644 index 000000000..c093bd9ab --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/MainWindow.xaml.cpp @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" +#include "MainWindow.xaml.h" +#if __has_include("MainWindow.g.cpp") +#include "MainWindow.g.cpp" +#endif + +// This include file is needed for the XAML Native Window Interop. +#include "microsoft.ui.xaml.window.h" + +namespace winrt +{ + using namespace ::winrt::Microsoft::UI; + using namespace ::winrt::Microsoft::UI::Windowing; + using namespace ::winrt::Microsoft::UI::Xaml; + using namespace ::winrt::Microsoft::UI::Xaml::Input; + using namespace ::winrt::Microsoft::UI::Xaml::Media; + using namespace ::winrt::Windows::Foundation; +} + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace winrt::CustomEditControlCpp::implementation +{ + MainWindow::MainWindow() + { + InitializeComponent(); + + MyCustomEditControl().SetAppWindow(GetAppWindowId()); + + Content().PointerPressed({ this, &MainWindow::PointerPressed }); + } + + winrt::WindowId MainWindow::GetAppWindowId() + { + winrt::AppWindow appWindow = nullptr; + + // Get the HWND for the XAML Window + HWND hWnd; + winrt::Window window = this->try_as(); + window.as()->get_WindowHandle(&hWnd); + + // Get the WindowId for the HWND + return GetWindowIdFromWindow(hWnd); + } + + static winrt::Rect GetElementRect(winrt::UIElement element) + { + winrt::GeneralTransform transform = element.TransformToVisual(nullptr); + winrt::Point point = transform.TransformPoint(winrt::Point()); + return winrt::Rect(point, element.ActualSize()); + } + + void MainWindow::PointerPressed(const winrt::IInspectable& /*sender*/, const winrt::PointerRoutedEventArgs& args) + { + winrt::Rect contentRect = GetElementRect(MyCustomEditControl()); + if (winrt::RectHelper::Contains(contentRect, args.GetCurrentPoint(nullptr).Position())) + { + // The user tapped inside the control. Give it focus. + MyCustomEditControl().SetInternalFocus(); + + // Tell XAML that our custom element has focus, so we don't have two + // focus elements. That is the extent of our integration with XAML focus. + MyCustomEditControl().Focus(winrt::FocusState::Programmatic); + + // A more complete custom control would move the caret to the + // pointer position. It would also provide some way to select + // text via touch. We do neither in this sample. + } + else + { + // The user tapped outside the control. Remove focus. + MyCustomEditControl().RemoveInternalFocus(); + } + } +} diff --git a/Samples/CustomEditControl/cpp-winui/MainWindow.xaml.h b/Samples/CustomEditControl/cpp-winui/MainWindow.xaml.h new file mode 100644 index 000000000..89a9bebf2 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/MainWindow.xaml.h @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once + +#include "MainWindow.g.h" + +namespace winrt::CustomEditControlCpp::implementation +{ + struct MainWindow : MainWindowT + { + MainWindow(); + void PointerPressed(const winrt::Windows::Foundation::IInspectable& /*sender*/, const winrt::Microsoft::UI::Xaml::Input::PointerRoutedEventArgs& /*args*/); + winrt::Microsoft::UI::WindowId GetAppWindowId(); + }; +} + +namespace winrt::CustomEditControlCpp::factory_implementation +{ + struct MainWindow : MainWindowT + { + }; +} diff --git a/Samples/CustomEditControl/cpp-winui/Package.appxmanifest b/Samples/CustomEditControl/cpp-winui/Package.appxmanifest new file mode 100644 index 000000000..63ac24b2a --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/Package.appxmanifest @@ -0,0 +1,48 @@ + + + + + + + + CustomEditControlCpp + Microsoft Corporation + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/CustomEditControl/cpp-winui/app.manifest b/Samples/CustomEditControl/cpp-winui/app.manifest new file mode 100644 index 000000000..a2b4b53fe --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/Samples/CustomEditControl/cpp-winui/packages.config b/Samples/CustomEditControl/cpp-winui/packages.config new file mode 100644 index 000000000..eb2857482 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Samples/CustomEditControl/cpp-winui/pch.cpp b/Samples/CustomEditControl/cpp-winui/pch.cpp new file mode 100644 index 000000000..40e691ba7 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/pch.cpp @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#include "pch.h" diff --git a/Samples/CustomEditControl/cpp-winui/pch.h b/Samples/CustomEditControl/cpp-winui/pch.h new file mode 100644 index 000000000..4c873aab6 --- /dev/null +++ b/Samples/CustomEditControl/cpp-winui/pch.h @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#pragma once +#include +#include +#include +#include + +// Undefine GetCurrentTime macro to prevent +// conflict with Storyboard::GetCurrentTime +#undef GetCurrentTime + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs.sln b/Samples/CustomEditControl/cs-winui/CustomEditControlCs.sln new file mode 100644 index 000000000..89bb39149 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomEditControlCs", "CustomEditControlCs\CustomEditControlCs.csproj", "{3318A3E2-990F-41F7-9D09-3D1CDDA260ED}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|arm64 = Debug|arm64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|arm64 = Release|arm64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Debug|arm64.ActiveCfg = Debug|arm64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Debug|arm64.Build.0 = Debug|arm64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Debug|arm64.Deploy.0 = Debug|arm64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Debug|x64.ActiveCfg = Debug|x64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Debug|x64.Build.0 = Debug|x64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Debug|x64.Deploy.0 = Debug|x64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Debug|x86.ActiveCfg = Debug|x86 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Debug|x86.Build.0 = Debug|x86 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Debug|x86.Deploy.0 = Debug|x86 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Release|arm64.ActiveCfg = Release|arm64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Release|arm64.Build.0 = Release|arm64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Release|arm64.Deploy.0 = Release|arm64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Release|x64.ActiveCfg = Release|x64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Release|x64.Build.0 = Release|x64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Release|x64.Deploy.0 = Release|x64 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Release|x86.ActiveCfg = Release|x86 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Release|x86.Build.0 = Release|x86 + {3318A3E2-990F-41F7-9D09-3D1CDDA260ED}.Release|x86.Deploy.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3846DF2F-35AB-47BE-BBC3-928BBE1159E7} + EndGlobalSection +EndGlobal diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/App.xaml b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/App.xaml new file mode 100644 index 000000000..26cd10f3c --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/App.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/App.xaml.cs b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/App.xaml.cs new file mode 100644 index 000000000..492817bce --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/App.xaml.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.UI.Xaml; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace CustomEditControlCs +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : Application + { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + this.InitializeComponent(); + } + + /// + /// Invoked when the application is launched normally by the end user. Other entry points + /// will be used such as when the application is launched to open a specific file. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + m_window = new MainWindow(); + m_window.Activate(); + } + + private Window m_window; + } +} diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/LockScreenLogo.scale-200.png b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 000000000..7440f0d4b Binary files /dev/null and b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/LockScreenLogo.scale-200.png differ diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/SplashScreen.scale-200.png b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/SplashScreen.scale-200.png new file mode 100644 index 000000000..32f486a86 Binary files /dev/null and b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/SplashScreen.scale-200.png differ diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Square150x150Logo.scale-200.png b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 000000000..53ee3777e Binary files /dev/null and b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Square150x150Logo.scale-200.png differ diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Square44x44Logo.scale-200.png b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 000000000..f713bba67 Binary files /dev/null and b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Square44x44Logo.scale-200.png differ diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 000000000..dc9f5bea0 Binary files /dev/null and b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/StoreLogo.png b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/StoreLogo.png new file mode 100644 index 000000000..a4586f26b Binary files /dev/null and b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/StoreLogo.png differ diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Wide310x150Logo.scale-200.png b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 000000000..8b4a5d0dd Binary files /dev/null and b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Assets/Wide310x150Logo.scale-200.png differ diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/CustomEditControl.xaml b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/CustomEditControl.xaml new file mode 100644 index 000000000..485df412d --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/CustomEditControl.xaml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + Full text: + Selection start index: + Selection end index: + + diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/CustomEditControl.xaml.cs b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/CustomEditControl.xaml.cs new file mode 100644 index 000000000..c04224705 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/CustomEditControl.xaml.cs @@ -0,0 +1,557 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.UI; +using Microsoft.UI.Input; +using Microsoft.UI.Windowing; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using System; +using Windows.Foundation; +using Windows.System; +using Windows.UI.Core; +using Windows.UI.Text.Core; +using Windows.UI.ViewManagement; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace CustomEditControlCs +{ + public sealed partial class CustomEditControl : UserControl + { + // The _editContext lets us communicate with the input system. + CoreTextEditContext _editContext; + + // We will use a plain text string to represent the + // content of the custom text edit control. + string _text = String.Empty; + + // If the _selection starts and ends at the same point, + // then it represents the location of the caret (insertion point). + CoreTextRange _selection; + + // _internalFocus keeps track of whether our control acts like it has focus. + bool _internalFocus = false; + + // If there is a nonempty selection, then _extendingLeft is true if the user + // is using shift+arrow to adjust the starting point of the selection, + // or false if the user is adjusting the ending point of the selection. + bool _extendingLeft = false; + + // The input pane object indicates the visibility of the on screen keyboard. + // Apps can also ask the keyboard to show or hide. + InputPane _inputPane; + + // The WindowId for the AppWindow that contains our control. + WindowId _windowId; + + public CustomEditControl() + { + this.InitializeComponent(); + + // The CoreTextEditContext processes text input, but other keys are + // the app's responsibility. + + // Create a CoreTextEditContext for our custom edit control. + CoreTextServicesManager manager = CoreTextServicesManager.GetForCurrentView(); + _editContext = manager.CreateEditContext(); + + // For demonstration purposes, this sample sets the Input Pane display policy to Manual + // so that it can manually show the software keyboard when the control gains focus and + // dismiss it when the control loses focus. If you leave the policy as Automatic, then + // the system will hide and show the Input Pane for you. Note that on Desktop, you will + // need to implement the UIA text pattern to get expected automatic behavior. + _editContext.InputPaneDisplayPolicy = CoreTextInputPaneDisplayPolicy.Manual; + + // Set the input scope to Text because this text box is for any text. + // This also informs software keyboards to show their regular + // text entry layout. There are many other input scopes and each will + // inform a keyboard layout and text behavior. + _editContext.InputScope = CoreTextInputScope.Text; + + // The system raises this event to request a specific range of text. + _editContext.TextRequested += EditContext_TextRequested; + + // The system raises this event to request the current selection. + _editContext.SelectionRequested += EditContext_SelectionRequested; + + // The system raises this event when it wants the edit control to remove focus. + _editContext.FocusRemoved += EditContext_FocusRemoved; + + // The system raises this event to update text in the edit control. + _editContext.TextUpdating += EditContext_TextUpdating; + + // The system raises this event to change the selection in the edit control. + _editContext.SelectionUpdating += EditContext_SelectionUpdating; + + // The system raises this event when it wants the edit control + // to apply formatting on a range of text. + _editContext.FormatUpdating += EditContext_FormatUpdating; + + // The system raises this event to request layout information. + // This is used to help choose a position for the IME candidate window. + _editContext.LayoutRequested += EditContext_LayoutRequested; + + // The system raises this event to notify the edit control + // that the string composition has started. + _editContext.CompositionStarted += EditContext_CompositionStarted; + + // The system raises this event to notify the edit control + // that the string composition is finished. + _editContext.CompositionCompleted += EditContext_CompositionCompleted; + + // The system raises this event when the NotifyFocusLeave operation has + // completed. Our sample does not use this event. + // _editContext.NotifyFocusLeaveCompleted += EditContext_NotifyFocusLeaveCompleted; + + // Set our initial UI. + UpdateTextUI(); + UpdateFocusUI(); + } + + public void SetAppWindow(WindowId windowId) + { + _windowId = windowId; + + // Get the Input Pane so we can programmatically hide and show it. + IntPtr hWnd = Win32Interop.GetWindowFromWindowId(_windowId); + if (hWnd != IntPtr.Zero) + { + _inputPane = InputPaneInterop.GetForWindow(hWnd); + } + } + + #region Focus management + public void SetInternalFocus() + { + if (!_internalFocus) + { + // Update internal notion of focus. + _internalFocus = true; + + // Update the UI. + UpdateTextUI(); + UpdateFocusUI(); + + // Notify the CoreTextEditContext that the edit context has focus, + // so it should start processing text input. + _editContext.NotifyFocusEnter(); + } + + // Ask the software keyboard to show. The system will ultimately decide if it will show. + // For example, it will not show if there is a keyboard attached. + _inputPane.TryShow(); + + } + + public void RemoveInternalFocus() + { + if (_internalFocus) + { + //Notify the system that this edit context is no longer in focus + _editContext.NotifyFocusLeave(); + + RemoveInternalFocusWorker(); + } + } + + void RemoveInternalFocusWorker() + { + //Update the internal notion of focus + _internalFocus = false; + + // Ask the software keyboard to dismiss. + _inputPane.TryHide(); + + // Update our UI. + UpdateTextUI(); + UpdateFocusUI(); + } + + void EditContext_FocusRemoved(CoreTextEditContext sender, object args) + { + RemoveInternalFocusWorker(); + } + + #endregion + + #region Text management + // Replace the text in the specified range. + void ReplaceText(CoreTextRange modifiedRange, string text) + { + // Modify the internal text store. + _text = _text.Substring(0, modifiedRange.StartCaretPosition) + + text + + _text.Substring(modifiedRange.EndCaretPosition); + + // Move the caret to the end of the replacement text. + _selection.StartCaretPosition = modifiedRange.StartCaretPosition + text.Length; + _selection.EndCaretPosition = _selection.StartCaretPosition; + + // Update the selection of the edit context. There is no need to notify the system + // of the selection change because we are going to call NotifyTextChanged soon. + SetSelection(_selection); + + // Let the CoreTextEditContext know what changed. + _editContext.NotifyTextChanged(modifiedRange, text.Length, _selection); + } + + bool HasSelection() + { + return _selection.StartCaretPosition != _selection.EndCaretPosition; + } + + // Change the selection without notifying CoreTextEditContext of the new selection. + void SetSelection(CoreTextRange selection) + { + // Modify the internal selection. + _selection = selection; + + //Update the UI to show the new selection. + UpdateTextUI(); + } + + // Change the selection and notify CoreTextEditContext of the new selection. + void SetSelectionAndNotify(CoreTextRange selection) + { + SetSelection(selection); + _editContext.NotifySelectionChanged(_selection); + } + + // Return the specified range of text. Note that the system may ask for more text + // than exists in the text buffer. + void EditContext_TextRequested(CoreTextEditContext sender, CoreTextTextRequestedEventArgs args) + { + CoreTextTextRequest request = args.Request; + request.Text = _text.Substring( + request.Range.StartCaretPosition, + Math.Min(request.Range.EndCaretPosition, _text.Length) - request.Range.StartCaretPosition); + } + + // Return the current selection. + void EditContext_SelectionRequested(CoreTextEditContext sender, CoreTextSelectionRequestedEventArgs args) + { + CoreTextSelectionRequest request = args.Request; + request.Selection = _selection; + } + + void EditContext_TextUpdating(CoreTextEditContext sender, CoreTextTextUpdatingEventArgs args) + { + CoreTextRange range = args.Range; + string newText = args.Text; + CoreTextRange newSelection = args.NewSelection; + + // Modify the internal text store. + _text = _text.Substring(0, range.StartCaretPosition) + + newText + + _text.Substring(Math.Min(_text.Length, range.EndCaretPosition)); + + // You can set the proper font or direction for the updated text based on the language by checking + // args.InputLanguage. We will not do that in this sample. + + // Modify the current selection. + newSelection.EndCaretPosition = newSelection.StartCaretPosition; + + // Update the selection of the edit context. There is no need to notify the system + // because the system itself changed the selection. + SetSelection(newSelection); + } + + void EditContext_SelectionUpdating(CoreTextEditContext sender, CoreTextSelectionUpdatingEventArgs args) + { + // Set the new selection to the value specified by the system. + CoreTextRange range = args.Selection; + + // Update the selection of the edit context. There is no need to notify the system + // because the system itself changed the selection. + SetSelection(range); + } + #endregion + + #region Formatting and layout + void EditContext_FormatUpdating(CoreTextEditContext sender, CoreTextFormatUpdatingEventArgs args) + { + // The following code specifies how you would apply any formatting to the specified range of text + // For this sample, we do not make any changes to the format. + + // Apply text color if specified. + // A null value indicates that the default should be used. + if (args.TextColor != null) + { + //InternalSetTextColor(args.Range, args.TextColor.Value); + } + else + { + //InternalSetDefaultTextColor(args.Range); + } + + // Apply background color if specified. + // A null value indicates that the default should be used. + if (args.BackgroundColor != null) + { + //InternalSetBackgroundColor(args.Range, args.BackgroundColor.Value); + } + else + { + //InternalSetDefaultBackgroundColor(args.Range); + } + + // Apply underline if specified. + // A null value indicates that the default should be used. + if (args.UnderlineType != null) + { + //TextDecoration underline = new TextDecoration(args.Range,args.UnderlineType.Value,args.UnderlineColor.Value); + + //InternalAddTextDecoration(underline); + } + else + { + //InternalRemoveTextDecoration(args.Range); + } + } + + static Rect ScaleRect(Rect rect, double scale) + { + rect.X *= scale; + rect.Y *= scale; + rect.Width *= scale; + rect.Height *= scale; + return rect; + } + + void EditContext_LayoutRequested(CoreTextEditContext sender, CoreTextLayoutRequestedEventArgs args) + { + CoreTextLayoutRequest request = args.Request; + + // Get the screen coordinates of the entire control and the selected text. + // This information is used to position the IME candidate window. + + // First, get the coordinates of the edit control and the selection + // relative to the Window. + Rect contentRect = MainWindow.GetElementRect(ContentPanel); + Rect selectionRect = MainWindow.GetElementRect(SelectionText); + + // Next, convert to screen coordinates in view pixels. + AppWindow window = AppWindow.GetFromWindowId(_windowId); + contentRect.X += window.Position.X; + contentRect.Y += window.Position.Y; + selectionRect.X += window.Position.X; + selectionRect.Y += window.Position.Y; + + // Finally, scale up to raw pixels. + double scaleFactor = XamlRoot.RasterizationScale; + contentRect = ScaleRect(contentRect, scaleFactor); + selectionRect = ScaleRect(selectionRect, scaleFactor); + + // This is the bounds of the selection. + // Note: If you return bounds with 0 width and 0 height, candidates will not appear while typing. + request.LayoutBounds.TextBounds = selectionRect; + + //This is the bounds of the whole control + request.LayoutBounds.ControlBounds = contentRect; + } + #endregion + + #region Input + // This indicates that an IME has started composition. If there is no handler for this event, + // then composition will not start. + void EditContext_CompositionStarted(CoreTextEditContext sender, CoreTextCompositionStartedEventArgs args) + { + } + + void EditContext_CompositionCompleted(CoreTextEditContext sender, CoreTextCompositionCompletedEventArgs args) + { + } + + protected override void OnCharacterReceived(CharacterReceivedRoutedEventArgs e) + { + // Do not process keyboard input if the custom edit control does not + // have focus. + if (!_internalFocus) + { + return; + } + + if (!Char.IsControl(e.Character)) + { + ReplaceText(_selection, e.Character.ToString()); + UpdateTextUI(); + } + } + + protected override void OnKeyDown(KeyRoutedEventArgs args) + { + // Do not process keyboard input if the custom edit control does not + // have focus. + if (!_internalFocus) + { + return; + } + + // This holds the range we intend to operate on, or which we intend + // to become the new selection. Start with the current selection. + CoreTextRange range = _selection; + + // For the purpose of this sample, we will support only the left and right + // arrow keys and the backspace key. A more complete text edit control + // would also handle keys like Home, End, and Delete, as well as + // hotkeys like Ctrl+V to paste. + // + // Note that this sample does not properly handle surrogate pairs + // nor does it handle grapheme clusters. + + switch (args.Key) + { + // Backspace + case VirtualKey.Back: + // If there is a selection, then delete the selection. + if (HasSelection()) + { + // Set the text in the selection to nothing. + ReplaceText(range, ""); + } + else + { + // Delete the character to the left of the caret, if one exists, + // by creating a range that encloses the character to the left + // of the caret, and setting the contents of that range to nothing. + range.StartCaretPosition = Math.Max(0, range.StartCaretPosition - 1); + ReplaceText(range, ""); + } + break; + + // Left arrow + case VirtualKey.Left: + // If the shift key is down, then adjust the size of the selection. + if (InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down)) + { + // If this is the start of a selection, then remember which edge we are adjusting. + if (!HasSelection()) + { + _extendingLeft = true; + } + + // Adjust the selection and notify CoreTextEditContext. + AdjustSelectionEndpoint(-1); + } + else + { + // The shift key is not down. If there was a selection, then snap the + // caret at the left edge of the selection. + if (HasSelection()) + { + range.EndCaretPosition = range.StartCaretPosition; + SetSelectionAndNotify(range); + } + else + { + // There was no selection. Move the caret left one code unit if possible. + range.StartCaretPosition = Math.Max(0, range.StartCaretPosition - 1); + range.EndCaretPosition = range.StartCaretPosition; + SetSelectionAndNotify(range); + } + } + break; + + // Right arrow + case VirtualKey.Right: + // If the shift key is down, then adjust the size of the selection. + if (InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down)) + { + // If this is the start of a selection, then remember which edge we are adjusting. + if (!HasSelection()) + { + _extendingLeft = false; + } + + // Adjust the selection and notify CoreTextEditContext. + AdjustSelectionEndpoint(+1); + } + else + { + // The shift key is not down. If there was a selection, then snap the + // caret at the right edge of the selection. + if (HasSelection()) + { + range.StartCaretPosition = range.EndCaretPosition; + SetSelectionAndNotify(range); + } + else + { + // There was no selection. Move the caret right one code unit if possible. + range.StartCaretPosition = Math.Min(_text.Length, range.StartCaretPosition + 1); + range.EndCaretPosition = range.StartCaretPosition; + SetSelectionAndNotify(range); + } + } + break; + } + } + + // Adjust the active endpoint of the selection in the specified direction. + void AdjustSelectionEndpoint(int direction) + { + CoreTextRange range = _selection; + if (_extendingLeft) + { + range.StartCaretPosition = Math.Max(0, range.StartCaretPosition + direction); + } + else + { + range.EndCaretPosition = Math.Min(_text.Length, range.EndCaretPosition + direction); + } + + SetSelectionAndNotify(range); + } + #endregion + + #region UI + // Helper function to put a zero-width non-breaking space at the end of a string. + // This prevents TextBlock from trimming trailing spaces. + static string PreserveTrailingSpaces(string s) + { + return s + "\ufeff"; + } + + void UpdateFocusUI() + { + BorderPanel.BorderBrush = _internalFocus ? new SolidColorBrush(Microsoft.UI.Colors.Green) : null; + } + + void UpdateTextUI() + { + // The raw materials we have are a string (_text) and information about + // where the caret/selection is (_selection). We can render the control + // any way we like. + + BeforeSelectionText.Text = PreserveTrailingSpaces(_text.Substring(0, _selection.StartCaretPosition)); + if (HasSelection()) + { + // There is a selection. Draw that. + CaretText.Visibility = Visibility.Collapsed; + SelectionText.Text = PreserveTrailingSpaces( + _text.Substring(_selection.StartCaretPosition, _selection.EndCaretPosition - _selection.StartCaretPosition)); + } + else + { + // There is no selection. Remove it. + SelectionText.Text = ""; + + // Draw the caret if we have focus. + CaretText.Visibility = _internalFocus ? Visibility.Visible : Visibility.Collapsed; + } + + AfterSelectionText.Text = PreserveTrailingSpaces(_text.Substring(_selection.EndCaretPosition)); + + // Update statistics for demonstration purposes. + FullText.Text = _text; + SelectionStartIndexText.Text = _selection.StartCaretPosition.ToString(); + SelectionEndIndexText.Text = _selection.EndCaretPosition.ToString(); + } + + #endregion + } +} diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/CustomEditControlCs.csproj b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/CustomEditControlCs.csproj new file mode 100644 index 000000000..8abe87001 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/CustomEditControlCs.csproj @@ -0,0 +1,45 @@ + + + WinExe + net6.0-windows10.0.19041.0 + 10.0.17763.0 + CustomEditControlCs + app.manifest + x86;x64;arm64 + win10-x86;win10-x64;win10-arm64 + win10-$(Platform).pubxml + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + MSBuild:Compile + + + diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/MainWindow.xaml b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/MainWindow.xaml new file mode 100644 index 000000000..777ac1885 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/MainWindow.xaml @@ -0,0 +1,30 @@ + + + + + + + + + Text input + + + When the custom text control has focus, it draws with a green border. + The caret is represented by a globe. + See the README for this sample for further instructions. + + + + + + + diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/MainWindow.xaml.cs b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/MainWindow.xaml.cs new file mode 100644 index 000000000..e7edd86f0 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/MainWindow.xaml.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using System; +using Windows.Foundation; +using WinRT.Interop; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace CustomEditControlCs +{ + /// + /// An empty window that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + Content.PointerPressed += OnPointerPressed; + + IntPtr hWnd = WindowNative.GetWindowHandle((Window)this); + WindowId windowId = Win32Interop.GetWindowIdFromWindow(hWnd); + MyCustomEditControl.SetAppWindow(windowId); + } + + public static Rect GetElementRect(FrameworkElement element) + { + GeneralTransform transform = element.TransformToVisual(null); + Point point = transform.TransformPoint(new Point()); + return new Rect(point, new Size(element.ActualWidth, element.ActualHeight)); + } + + private void OnPointerPressed(object sender, PointerRoutedEventArgs e) + { + Rect contentRect = GetElementRect(MyCustomEditControl); + if (contentRect.Contains(e.GetCurrentPoint(null).Position)) + { + // The user tapped inside the control. Give it focus. + MyCustomEditControl.SetInternalFocus(); + + // Tell XAML that this element has focus, so we don't have two + // focus elements. That is the extent of our integration with XAML focus. + MyCustomEditControl.Focus(FocusState.Programmatic); + + // A more complete custom control would move the caret to the + // pointer position. It would also provide some way to select + // text via touch. We do neither in this sample. + + } + else + { + // The user tapped outside the control. Remove focus. + MyCustomEditControl.RemoveInternalFocus(); + } + } + } +} diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Package.appxmanifest b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Package.appxmanifest new file mode 100644 index 000000000..f558c6141 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Package.appxmanifest @@ -0,0 +1,48 @@ + + + + + + + + CustomEditControlCs + Microsoft Corporation + Assets\StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/PublishProfiles/win10-arm64.pubxml b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/PublishProfiles/win10-arm64.pubxml new file mode 100644 index 000000000..a132e44c6 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/PublishProfiles/win10-arm64.pubxml @@ -0,0 +1,20 @@ + + + + + FileSystem + arm64 + win10-arm64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + true + False + False + True + + + \ No newline at end of file diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/PublishProfiles/win10-x64.pubxml b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/PublishProfiles/win10-x64.pubxml new file mode 100644 index 000000000..26ea7e55c --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/PublishProfiles/win10-x64.pubxml @@ -0,0 +1,20 @@ + + + + + FileSystem + x64 + win10-x64 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + true + False + False + True + + + \ No newline at end of file diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/PublishProfiles/win10-x86.pubxml b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/PublishProfiles/win10-x86.pubxml new file mode 100644 index 000000000..34d14d4d4 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/PublishProfiles/win10-x86.pubxml @@ -0,0 +1,20 @@ + + + + + FileSystem + x86 + win10-x86 + bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\ + true + False + False + True + + + \ No newline at end of file diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/launchSettings.json b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/launchSettings.json new file mode 100644 index 000000000..4303eed02 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "CustomEditControlSampleCs (Package)": { + "commandName": "MsixPackage" + }, + "CustomEditControlSampleCs (Unpackaged)": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/Samples/CustomEditControl/cs-winui/CustomEditControlCs/app.manifest b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/app.manifest new file mode 100644 index 000000000..63509d8c6 --- /dev/null +++ b/Samples/CustomEditControl/cs-winui/CustomEditControlCs/app.manifest @@ -0,0 +1,15 @@ + + + + + + + + true/PM + PerMonitorV2, PerMonitor + + + diff --git a/SamplesCI-CustomEditControl.yml b/SamplesCI-CustomEditControl.yml new file mode 100644 index 000000000..a3c10be87 --- /dev/null +++ b/SamplesCI-CustomEditControl.yml @@ -0,0 +1,7 @@ +# Please see https://www.osgwiki.com/wiki/Windows_App_SDK_-_How_to_build_and_use_the_pipelines +# for information on how to use the pipelines +name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) +stages: +- template: WindowsAppSDK-SamplesCI.yml + parameters: + FeatureDirectory: "CustomEditControl" \ No newline at end of file