Browse Source

Merge branch 'master' into flyout-api

Max Katz 2 years ago
parent
commit
7947ec9915
25 changed files with 619 additions and 145 deletions
  1. 1 0
      azure-pipelines-integrationtests.yml
  2. 12 0
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  3. 46 0
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.h
  4. 41 0
      native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm
  5. 20 0
      native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h
  6. 2 4
      native/Avalonia.Native/src/OSX/AvnView.h
  7. 72 13
      native/Avalonia.Native/src/OSX/AvnView.mm
  8. 4 0
      native/Avalonia.Native/src/OSX/WindowBaseImpl.h
  9. 10 0
      native/Avalonia.Native/src/OSX/WindowBaseImpl.mm
  10. 1 1
      samples/ControlCatalog/Pages/FlyoutsPage.axaml
  11. 33 17
      src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs
  12. 1 1
      src/Avalonia.Base/Styling/DescendentSelector.cs
  13. 11 16
      src/Avalonia.Base/Styling/OrSelector.cs
  14. 2 7
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  15. 7 57
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  16. 1 1
      src/Avalonia.Controls/TextBox.cs
  17. 48 10
      src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
  18. 118 0
      src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs
  19. 10 1
      src/Avalonia.Native/WindowImpl.cs
  20. 17 0
      src/Avalonia.Native/avn.idl
  21. 12 10
      src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs
  22. 69 6
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  23. 15 0
      tests/Avalonia.Benchmarks/Properties/launchSettings.json
  24. 47 1
      tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs
  25. 19 0
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

+ 1 - 0
azure-pipelines-integrationtests.yml

@@ -24,6 +24,7 @@ jobs:
       fi
       sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
       pkill node
+      pkill testmanagerd
       appium > appium.out &
       pkill IntegrationTestApp
       ./build.sh CompileNative

+ 12 - 0
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@@ -44,6 +44,9 @@
 		5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
 		5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
 		855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */ = {isa = PBXBuildFile; fileRef = 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */; };
+		8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */; };
+		8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */; };
+		8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */; };
 		AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; };
 		AB1E522C217613570091CD71 /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB1E522B217613570091CD71 /* OpenGL.framework */; };
 		AB661C1E2148230F00291242 /* AppKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AB661C1D2148230F00291242 /* AppKit.framework */; };
@@ -97,6 +100,9 @@
 		5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; };
 		5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
 		855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformBehaviorInhibition.mm; sourceTree = "<group>"; };
+		8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethodDelegate.h; sourceTree = "<group>"; };
+		8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnTextInputMethod.h; sourceTree = "<group>"; };
+		8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnTextInputMethod.mm; sourceTree = "<group>"; };
 		AB00E4F62147CA920032A60A /* main.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
 		AB1E522B217613570091CD71 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = System/Library/Frameworks/OpenGL.framework; sourceTree = SDKROOT; };
 		AB661C1D2148230F00291242 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; };
@@ -143,6 +149,9 @@
 			isa = PBXGroup;
 			children = (
 				855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */,
+				8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */,
+				8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */,
+				8D300D64292D0A6800320C49 /* AvnTextInputMethod.h */,
 				BC11A5BC2608D58F0017BAD0 /* automation.h */,
 				BC11A5BD2608D58F0017BAD0 /* automation.mm */,
 				1A1852DB23E05814008F0DED /* deadlock.mm */,
@@ -213,6 +222,8 @@
 				1839171DCC651B0638603AC4 /* INSWindowHolder.h in Headers */,
 				183919D91DB9AAB5D700C2EA /* WindowImpl.h in Headers */,
 				18391CF07316F819E76B617C /* IWindowStateChanged.h in Headers */,
+				8D300D65292D0A6800320C49 /* AvnTextInputMethod.h in Headers */,
+				8D2F3512292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h in Headers */,
 				18391C28BF1823B5464FDD36 /* ResizeScope.h in Headers */,
 				18391ED5F611FF62C45F196D /* AvnView.h in Headers */,
 				18391E1381E2D5BFD60265A9 /* AutoFitContentView.h in Headers */,
@@ -293,6 +304,7 @@
 				37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
 				855EDC9F28C6546F00807998 /* PlatformBehaviorInhibition.mm in Sources */,
 				520624B322973F4100C4DCEF /* menu.mm in Sources */,
+				8D300D69292E1E5D00320C49 /* AvnTextInputMethod.mm in Sources */,
 				37A517B32159597E00FBA241 /* Screens.mm in Sources */,
 				1AFD334123E03C4F0042899B /* controlhost.mm in Sources */,
 				1A465D10246AB61600C5858B /* dnd.mm in Sources */,

+ 46 - 0
native/Avalonia.Native/src/OSX/AvnTextInputMethod.h

@@ -0,0 +1,46 @@
+//
+//  AvnTextInputMethod.h
+//  Avalonia.Native.OSX
+//
+//  Created by Benedikt Stebner on 22.11.22.
+//  Copyright © 2022 Avalonia. All rights reserved.
+//
+
+#ifndef AvnTextInputMethod_h
+#define AvnTextInputMethod_h
+
+#import <Foundation/Foundation.h>
+
+#include "com.h"
+#include "comimpl.h"
+#include "avalonia-native.h"
+#import "AvnTextInputMethodDelegate.h"
+
+class AvnTextInputMethod: public virtual ComObject, public virtual IAvnTextInputMethod{
+private:
+    id<AvnTextInputMethodDelegate> _inputMethodDelegate;
+public:
+    FORWARD_IUNKNOWN()
+    
+    BEGIN_INTERFACE_MAP()
+    INTERFACE_MAP_ENTRY(IAvnTextInputMethod, IID_IAvnTextInputMethod)
+    END_INTERFACE_MAP()
+    
+    virtual ~AvnTextInputMethod();
+    
+    AvnTextInputMethod(id<AvnTextInputMethodDelegate> inputMethodDelegate);
+    
+    bool IsActive ();
+    
+    HRESULT SetClient (IAvnTextInputMethodClient* client) override;
+    
+    virtual void Reset () override;
+    
+    virtual void SetCursorRect (AvnRect rect) override;
+    
+    virtual void SetSurroundingText (char* text, int anchorOffset, int cursorOffset) override;
+    
+public:
+    ComPtr<IAvnTextInputMethodClient> Client;
+};
+#endif /* AvnTextInputMethod_h */

+ 41 - 0
native/Avalonia.Native/src/OSX/AvnTextInputMethod.mm

@@ -0,0 +1,41 @@
+//
+//  AvnTextInputMethod.mm
+//  Avalonia.Native.OSX
+//
+//  Created by Benedikt Stebner on 23.11.22.
+//  Copyright © 2022 Avalonia. All rights reserved.
+//
+
+#include "AvnTextInputMethod.h"
+
+AvnTextInputMethod::~AvnTextInputMethod() {
+    Client = nullptr;
+}
+
+AvnTextInputMethod::AvnTextInputMethod(id<AvnTextInputMethodDelegate> inputMethodDelegate) {
+    _inputMethodDelegate = inputMethodDelegate;
+}
+
+bool AvnTextInputMethod::IsActive() {
+    return Client != nullptr;
+}
+
+HRESULT AvnTextInputMethod::SetClient(IAvnTextInputMethodClient *client) {
+    START_COM_CALL;
+    
+    Client = client;
+    
+    return S_OK;
+}
+
+void AvnTextInputMethod::Reset() {
+}
+
+void AvnTextInputMethod::SetSurroundingText(char* text, int anchorOffset, int cursorOffset) {
+    [_inputMethodDelegate setText:[NSString stringWithUTF8String:text]];
+    [_inputMethodDelegate setSelection: anchorOffset : cursorOffset];
+}
+
+void AvnTextInputMethod::SetCursorRect(AvnRect rect) {
+    [_inputMethodDelegate setCursorRect: rect];
+}

+ 20 - 0
native/Avalonia.Native/src/OSX/AvnTextInputMethodDelegate.h

@@ -0,0 +1,20 @@
+//
+//  AvnTextInputMethodHost.h
+//  Avalonia.Native.OSX
+//
+//  Created by Benedikt Stebner on 24.11.22.
+//  Copyright © 2022 Avalonia. All rights reserved.
+//
+
+#ifndef AvnTextInputMethodHost_h
+#define AvnTextInputMethodHost_h
+
+@protocol AvnTextInputMethodDelegate
+@required
+-(void) setText:(NSString* _Nonnull) text;
+-(void) setCursorRect:(AvnRect) cursorRect;
+-(void) setSelection: (int) start : (int) end;
+
+@end
+
+#endif /* AvnTextInputMethodHost_h */

+ 2 - 4
native/Avalonia.Native/src/OSX/AvnView.h

@@ -5,8 +5,6 @@
 #pragma once
 #import <Foundation/Foundation.h>
 
-
-#import <Foundation/Foundation.h>
 #import <AppKit/AppKit.h>
 #include "common.h"
 #include "WindowImpl.h"
@@ -14,7 +12,7 @@
 
 @class AvnAccessibilityElement;
 
-@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
+@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination, AvnTextInputMethodDelegate>
 -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
 -(NSEvent* _Nonnull) lastMouseDownEvent;
 -(AvnPoint) translateLocalPoint:(AvnPoint)pt;
@@ -24,4 +22,4 @@
 -(AvnPlatformResizeReason) getResizeReason;
 -(void) setResizeReason:(AvnPlatformResizeReason)reason;
 + (AvnPoint)toAvnPoint:(CGPoint)p;
-@end
+@end

+ 72 - 13
native/Avalonia.Native/src/OSX/AvnView.mm

@@ -12,6 +12,7 @@
 {
     ComPtr<WindowBaseImpl> _parent;
     NSTrackingArea* _area;
+    NSMutableAttributedString* _markedText;
     bool _isLeftPressed, _isMiddlePressed, _isRightPressed, _isXButton1Pressed, _isXButton2Pressed;
     AvnInputModifiers _modifierState;
     NSEvent* _lastMouseDownEvent;
@@ -20,6 +21,9 @@
     NSObject<IRenderTarget>* _renderTarget;
     AvnPlatformResizeReason _resizeReason;
     AvnAccessibilityElement* _accessibilityChild;
+    NSRect _cursorRect;
+    NSMutableString* _text;
+    NSRange _selection;
 }
 
 - (void)onClosed
@@ -518,7 +522,7 @@
 - (void)keyDown:(NSEvent *)event
 {
     [self keyboardEvent:event withType:KeyDown];
-    [[self inputContext] handleEvent:event];
+    _lastKeyHandled = [[self inputContext] handleEvent:event];
     [super keyDown:event];
 }
 
@@ -557,27 +561,50 @@
 
 - (BOOL)hasMarkedText
 {
-    return _lastKeyHandled;
+    return [_markedText length] > 0;
 }
 
 - (NSRange)markedRange
 {
+    if([_markedText length] > 0)
+        return NSMakeRange(0, [_markedText length] - 1);
     return NSMakeRange(NSNotFound, 0);
 }
 
 - (NSRange)selectedRange
 {
-    return NSMakeRange(NSNotFound, 0);
+    return _selection;
 }
 
 - (void)setMarkedText:(id)string selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange
 {
-
+    if([string isKindOfClass:[NSAttributedString class]])
+    {
+        _markedText = [[NSMutableAttributedString alloc] initWithAttributedString:string];
+    }
+    else
+    {
+        _markedText = [[NSMutableAttributedString alloc] initWithString:string];
+    }
+    
+    if(!_parent->InputMethod->IsActive()){
+        return;
+    }
+    
+    _parent->InputMethod->Client->SetPreeditText((char*)[_markedText.string UTF8String]);
 }
 
 - (void)unmarkText
 {
-
+    [[_markedText mutableString] setString:@""];
+    
+    if(!_parent->InputMethod->IsActive()){
+        return;
+    }
+    
+    _parent->InputMethod->Client->SetPreeditText(nullptr);
+    
+    [[self inputContext] discardMarkedText];
 }
 
 - (NSArray<NSString *> *)validAttributesForMarkedText
@@ -587,30 +614,38 @@
 
 - (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)range actualRange:(NSRangePointer)actualRange
 {
-    return [NSAttributedString new];
+    return nullptr;
 }
 
 - (void)insertText:(id)string replacementRange:(NSRange)replacementRange
 {
-    if(!_lastKeyHandled)
-    {
+    //[_text replaceCharactersInRange:replacementRange withString:string];
+    
+    [self unmarkText];
+    
+    //if(!_lastKeyHandled)
+    //{
         if(_parent != nullptr)
         {
             _lastKeyHandled = _parent->BaseEvents->RawTextInputEvent(0, [string UTF8String]);
         }
-    }
+    //}
+    
+    [[self inputContext] invalidateCharacterCoordinates];
 }
 
 - (NSUInteger)characterIndexForPoint:(NSPoint)point
 {
-    return 0;
+    return NSNotFound;
 }
 
 - (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange
 {
-    CGRect result = { 0 };
-
-    return result;
+    if(!_parent->InputMethod->IsActive()){
+        return NSZeroRect;
+    }
+    
+    return _cursorRect;
 }
 
 - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
@@ -715,4 +750,28 @@
     return [[self accessibilityChild] accessibilityFocusedUIElement];
 }
 
+- (void) setText:(NSString *)text{
+    [_text setString:text];
+    
+    [[self inputContext] discardMarkedText];
+}
+
+- (void) setSelection:(int)start :(int)end{
+    _selection = NSMakeRange(start, end - start);
+    
+    [[self inputContext] invalidateCharacterCoordinates];
+}
+
+- (void) setCursorRect:(AvnRect)rect{
+    NSRect cursorRect = ToNSRect(rect);
+    NSRect windowRectOnScreen = [[self window] convertRectToScreen:self.frame];
+    
+    windowRectOnScreen.size = cursorRect.size;
+    windowRectOnScreen.origin = NSMakePoint(windowRectOnScreen.origin.x + cursorRect.origin.x, windowRectOnScreen.origin.y + self.frame.size.height - cursorRect.origin.y - cursorRect.size.height);
+    
+    _cursorRect = windowRectOnScreen;
+    
+    [[self inputContext] invalidateCharacterCoordinates];
+}
+
 @end

+ 4 - 0
native/Avalonia.Native/src/OSX/WindowBaseImpl.h

@@ -8,6 +8,7 @@
 
 #include "rendertarget.h"
 #include "INSWindowHolder.h"
+#include "AvnTextInputMethod.h"
 
 @class AutoFitContentView;
 @class AvnMenu;
@@ -103,6 +104,8 @@ BEGIN_INTERFACE_MAP()
     id<AvnWindowProtocol> GetWindowProtocol ();
                            
     virtual void BringToFront ();
+                           
+    virtual HRESULT GetInputMethod(IAvnTextInputMethod **retOut) override;
 
 protected:
     virtual NSWindowStyleMask CalculateStyleMask() = 0;
@@ -130,6 +133,7 @@ public:
     NSObject <IRenderTarget> *renderTarget;
     NSWindow * Window;
     ComPtr<IAvnWindowBaseEvents> BaseEvents;
+    ComPtr<AvnTextInputMethod> InputMethod;
     AvnView *View;
 };
 

+ 10 - 0
native/Avalonia.Native/src/OSX/WindowBaseImpl.mm

@@ -15,6 +15,7 @@
 #import "WindowProtocol.h"
 #import "WindowInterfaces.h"
 #include "WindowBaseImpl.h"
+#include "AvnTextInputMethod.h"
 
 
 WindowBaseImpl::~WindowBaseImpl() {
@@ -29,6 +30,7 @@ WindowBaseImpl::WindowBaseImpl(IAvnWindowBaseEvents *events, IAvnGlContext *gl,
     _glContext = gl;
     renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext:gl];
     View = [[AvnView alloc] initWithParent:this];
+    InputMethod = new AvnTextInputMethod(View);
     StandardContainer = [[AutoFitContentView new] initWithContent:View];
 
     lastPositionSet = { 0, 0 };
@@ -605,6 +607,14 @@ void WindowBaseImpl::BringToFront()
     // do nothing.
 }
 
+HRESULT WindowBaseImpl::GetInputMethod(IAvnTextInputMethod **retOut) {
+    START_COM_CALL;
+
+    *retOut = InputMethod;
+
+    return S_OK;
+}
+
 extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl)
 {
     @autoreleasepool

+ 1 - 1
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@@ -204,7 +204,7 @@
                                 </Flyout>
                             </Button.Flyout>
                         </Button>
-                        <Button Content="Placement=RightEdgeAlignedBottom">
+                        <Button Content="Placement=RightEdgeAlignedTop">
                             <Button.Flyout>
                                 <Flyout Placement="RightEdgeAlignedTop">
                                     <Panel Width="100" Height="100">

+ 33 - 17
src/Android/Avalonia.Android/Platform/Storage/AndroidStorageItem.cs

@@ -138,27 +138,43 @@ internal class AndroidStorageFolder : AndroidStorageItem, IStorageBookmarkFolder
             return Array.Empty<IStorageItem>();
         }
 
-        using var javaFile = new JavaFile(Uri.Path!);
+        List<IStorageItem> files = new List<IStorageItem>();
 
-        // Java file represents files AND directories. Don't be confused.
-        var files = await javaFile.ListFilesAsync().ConfigureAwait(false);
-        if (files is null)
+        var contentResolver = Activity.ContentResolver;
+        if (contentResolver == null)
         {
-            return Array.Empty<IStorageItem>();
+            return files;
         }
 
-        return files
-            .Select(f => (file: f, uri: AndroidUri.FromFile(f)))
-            .Where(t => t.uri is not null)
-            .Select(t => t.file switch
-            {
-                { IsFile: true } => (IStorageItem)new AndroidStorageFile(Activity, t.uri!),
-                { IsDirectory: true } => new AndroidStorageFolder(Activity, t.uri!, false),
-                _ => null
-            })
-            .Where(i => i is not null)
-            .ToArray()!;
-    }
+        var childrenUri = DocumentsContract.BuildChildDocumentsUriUsingTree(Uri!, DocumentsContract.GetTreeDocumentId(Uri));
+
+        var projection = new[]
+        {
+            DocumentsContract.Document.ColumnDocumentId,
+            DocumentsContract.Document.ColumnMimeType
+        };
+        if (childrenUri != null)
+        {
+            using var cursor = contentResolver.Query(childrenUri, projection, null, null, null);
+
+            if (cursor != null)
+                while (cursor.MoveToNext())
+                {
+                    var mime = cursor.GetString(1);
+                    var id = cursor.GetString(0);
+                    var uri = DocumentsContract.BuildDocumentUriUsingTree(Uri!, id);
+                    if (uri == null)
+                    {
+                        continue;
+                    }
+
+                    files.Add(mime == DocumentsContract.Document.MimeTypeDir ? new AndroidStorageFolder(Activity, uri, false) :
+                        new AndroidStorageFile(Activity, uri));
+                }
+        }
+
+        return files;
+    }       
 }
 
 internal sealed class WellKnownAndroidStorageFolder : AndroidStorageFolder

+ 1 - 1
src/Avalonia.Base/Styling/DescendentSelector.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Styling
 
         public DescendantSelector(Selector? parent)
         {
-            _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceeded by a selector.");
+            _parent = parent ?? throw new InvalidOperationException("Descendant selector must be preceded by a selector.");
         }
 
         /// <inheritdoc/>

+ 11 - 16
src/Avalonia.Base/Styling/OrSelector.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Styling
     /// <summary>
     /// The OR style selector.
     /// </summary>
-    internal class OrSelector : Selector
+    internal sealed class OrSelector : Selector
     {
         private readonly IReadOnlyList<Selector> _selectors;
         private string? _selectorString;
@@ -42,18 +42,7 @@ namespace Avalonia.Styling
         public override bool IsCombinator => false;
 
         /// <inheritdoc/>
-        public override Type? TargetType
-        {
-            get
-            {
-                if (_targetType == null)
-                {
-                    _targetType = EvaluateTargetType();
-                }
-
-                return _targetType;
-            }
-        }
+        public override Type? TargetType => _targetType ??= EvaluateTargetType();
 
         /// <inheritdoc/>
         public override string ToString(Style? owner)
@@ -71,7 +60,9 @@ namespace Avalonia.Styling
             var activators = new OrActivatorBuilder();
             var neverThisInstance = false;
 
-            for (var i = 0; i < _selectors.Count; i++)
+            var count = _selectors.Count;
+
+            for (var i = 0; i < count; i++)
             {
                 var match = _selectors[i].Match(control, parent, subscribe);
 
@@ -108,7 +99,9 @@ namespace Avalonia.Styling
 
         internal override void ValidateNestingSelector(bool inControlTheme)
         {
-            for (var i = 0; i < _selectors.Count; i++)
+            var count = _selectors.Count;
+
+            for (var i = 0; i < count; i++)
             {
                 _selectors[i].ValidateNestingSelector(inControlTheme);
             }
@@ -118,7 +111,9 @@ namespace Avalonia.Styling
         {
             Type? result = null;
 
-            for (var i = 0; i < _selectors.Count; i++)
+            var count = _selectors.Count;
+
+            for (var i = 0; i < count; i++)
             {
                 var selector = _selectors[i];
                 if (selector.TargetType == null)

+ 2 - 7
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@@ -11,7 +11,7 @@ namespace Avalonia.Styling
     /// A selector that matches the common case of a type and/or name followed by a collection of
     /// style classes and pseudoclasses.
     /// </summary>
-    internal class TypeNameAndClassSelector : Selector
+    internal sealed class TypeNameAndClassSelector : Selector
     {
         private readonly Selector? _previous;
         private List<string>? _classes;
@@ -85,12 +85,7 @@ namespace Avalonia.Styling
         /// <inheritdoc/>
         public override string ToString(Style? owner)
         {
-            if (_selectorString == null)
-            {
-                _selectorString = BuildSelectorString(owner);
-            }
-
-            return _selectorString;
+            return _selectorString ??= BuildSelectorString(owner);
         }
 
         /// <inheritdoc/>

+ 7 - 57
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -10,6 +10,7 @@ using Avalonia.Layout;
 using Avalonia.Media.Immutable;
 using Avalonia.Controls.Documents;
 using Avalonia.Input.TextInput;
+using Avalonia.Data;
 
 namespace Avalonia.Controls.Presenters
 {
@@ -52,7 +53,7 @@ namespace Avalonia.Controls.Presenters
             AvaloniaProperty.RegisterDirect<TextPresenter, string?>(
                 nameof(Text),
                 o => o.Text,
-                (o, v) => o.Text = v);
+                (o, v) => o.Text = v, defaultBindingMode: BindingMode.OneWay);
 
         /// <summary>
         /// Defines the <see cref="PreeditText"/> property.
@@ -107,7 +108,7 @@ namespace Avalonia.Controls.Presenters
         private int _selectionStart;
         private int _selectionEnd;
         private bool _caretBlink;
-        private string? _text;
+        internal string? _text;
         private TextLayout? _textLayout;
         private Size _constraint;
 
@@ -526,23 +527,6 @@ namespace Avalonia.Controls.Presenters
             }
         }
 
-        private string? GetText()
-        {
-            if (!string.IsNullOrEmpty(_preeditText))
-            {
-                if (string.IsNullOrEmpty(_text) || _caretIndex > _text.Length)
-                {
-                    return _preeditText;
-                }
-
-                var text = _text.Substring(0, _caretIndex) + _preeditText + _text.Substring(_caretIndex);
-
-                return text;
-            }
-
-            return _text;
-        }
-
         /// <summary>
         /// Creates the <see cref="TextLayout"/> used to render the text.
         /// </summary>
@@ -551,7 +535,7 @@ namespace Avalonia.Controls.Presenters
         {
             TextLayout result;
 
-            var text = GetText();
+            var text = _text;
 
             var typeface = new Typeface(FontFamily, FontStyle, FontWeight);
 
@@ -564,7 +548,7 @@ namespace Avalonia.Controls.Presenters
 
             var foreground = Foreground;
 
-            if(_compositionRegion != null)
+            if (_compositionRegion != null)
             {
                 var preeditHighlight = new ValueSpan<TextRunProperties>(_compositionRegion?.Start ?? 0, _compositionRegion?.Length ?? 0,
                         new GenericTextRunProperties(typeface, FontSize,
@@ -851,7 +835,7 @@ namespace Avalonia.Controls.Presenters
             CaretChanged();
         }
 
-        private void UpdateCaret(CharacterHit characterHit, bool updateCaretIndex = true)
+        internal void UpdateCaret(CharacterHit characterHit, bool notify = true)
         {
             _lastCharacterHit = characterHit;
 
@@ -879,7 +863,7 @@ namespace Avalonia.Controls.Presenters
                 CaretBoundsChanged?.Invoke(this, EventArgs.Empty);
             }
 
-            if (updateCaretIndex)
+            if (notify)
             {
                 SetAndRaise(CaretIndexProperty, ref _caretIndex, caretIndex);
             }
@@ -899,35 +883,6 @@ namespace Avalonia.Controls.Presenters
             _caretTimer.Tick -= CaretTimerTick;
         }
 
-        protected void OnPreeditTextChanged(string? oldValue, string? newValue)
-        {
-            InvalidateTextLayout();
-
-            if (string.IsNullOrEmpty(newValue))
-            {
-                UpdateCaret(_lastCharacterHit);
-            }
-            else
-            {
-                var textPosition = _caretIndex + newValue?.Length ?? 0;
-
-                var characterHit = GetCharacterHitFromTextPosition(textPosition);
-
-                UpdateCaret(characterHit, false);
-            }
-        }
-
-        private CharacterHit GetCharacterHitFromTextPosition(int textPosition)
-        {
-            var lineIndex = TextLayout.GetLineIndexFromCharacterIndex(textPosition, true);
-
-            var textLine = TextLayout.TextLines[lineIndex];
-
-            var characterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(textPosition - 1));
-
-            return characterHit;
-        }
-
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
         {
             base.OnPropertyChanged(change);
@@ -935,11 +890,6 @@ namespace Avalonia.Controls.Presenters
             switch (change.Property.Name)
             {
                 case nameof(PreeditText):
-                    {
-                        OnPreeditTextChanged(change.OldValue as string, change.NewValue as string);
-                        break;
-                    }
-
                 case nameof(CompositionRegion):
                 case nameof(Foreground):
                 case nameof(FontSize):

+ 1 - 1
src/Avalonia.Controls/TextBox.cs

@@ -1736,7 +1736,7 @@ namespace Avalonia.Controls
                 var lineIndex = _presenter.TextLayout.GetLineIndexFromCharacterIndex(caretIndex, false);
                 var textLine = textLines[lineIndex];
 
-                var textPosition = textLine.FirstTextSourceIndex + textLine.Length;
+                var textPosition = textLine.FirstTextSourceIndex + textLine.Length - textLine.NewLineLength;
 
                 _presenter.MoveCaretToTextPosition(textPosition, true);
             }

+ 48 - 10
src/Avalonia.Controls/TextBoxTextInputMethodClient.cs

@@ -1,11 +1,11 @@
 using System;
+using System.Diagnostics;
 using Avalonia.Controls.Presenters;
 using Avalonia.Input.TextInput;
+using Avalonia.Media;
 using Avalonia.Media.TextFormatting;
 using Avalonia.Threading;
 using Avalonia.Utilities;
-using Avalonia.VisualTree;
-using static System.Net.Mime.MediaTypeNames;
 
 namespace Avalonia.Controls
 {
@@ -77,7 +77,7 @@ namespace Avalonia.Controls
         {
             get => _textEditable; set
             {
-                if(_textEditable != null)
+                if (_textEditable != null)
                 {
                     _textEditable.TextChanged -= TextEditable_TextChanged;
                     _textEditable.SelectionChanged -= TextEditable_SelectionChanged;
@@ -86,7 +86,7 @@ namespace Avalonia.Controls
 
                 _textEditable = value;
 
-                if(_textEditable != null)
+                if (_textEditable != null)
                 {
                     _textEditable.TextChanged += TextEditable_TextChanged;
                     _textEditable.SelectionChanged += TextEditable_SelectionChanged;
@@ -112,7 +112,7 @@ namespace Avalonia.Controls
 
         private void TextEditable_SelectionChanged(object? sender, EventArgs e)
         {
-            if(_parent != null && _textEditable != null)
+            if (_parent != null && _textEditable != null)
             {
                 _parent.SelectionStart = _textEditable.SelectionStart;
                 _parent.SelectionEnd = _textEditable.SelectionEnd;
@@ -159,14 +159,51 @@ namespace Avalonia.Controls
 
         public event EventHandler? SurroundingTextChanged;
 
-        public void SetPreeditText(string? text)
+        private string? _presenterText;
+        private int _compositionStart;
+
+        public void SetPreeditText(string? preeditText)
         {
-            if (_presenter == null)
+            if (_presenter == null || _parent == null)
             {
                 return;
             }
 
-            _presenter.PreeditText = text;
+            if (_presenterText is null)
+            {
+                _presenterText = _parent.Text ?? "";
+                _compositionStart = _parent.CaretIndex;
+            }
+
+            var text = GetText(preeditText);
+
+            _presenter._text = text;
+
+            _presenter.PreeditText = preeditText;
+
+            _presenter.UpdateCaret(new CharacterHit(_compositionStart + (preeditText != null ? preeditText.Length : 0)), false);
+
+            if (string.IsNullOrEmpty(preeditText))
+            {
+                _presenterText = null;
+            }
+        }
+
+        private string? GetText(string? preeditText)
+        {
+            if (string.IsNullOrEmpty(preeditText))
+            {
+                return _presenterText;
+            }
+
+            if (string.IsNullOrEmpty(_presenterText))
+            {
+                return preeditText;
+            }
+
+            var text = _presenterText.Substring(0, _compositionStart) + preeditText + _presenterText.Substring(_compositionStart);
+
+            return text;
         }
 
         public void SetComposingRegion(TextRange? region)
@@ -175,6 +212,7 @@ namespace Avalonia.Controls
             {
                 return;
             }
+
             _presenter.CompositionRegion = region;
         }
 
@@ -256,9 +294,9 @@ namespace Avalonia.Controls
                 }
             }
 
-            if(e.Property == TextBox.TextProperty)
+            if (e.Property == TextBox.TextProperty)
             {
-                if(_textEditable != null)
+                if (_textEditable != null)
                 {
                     _textEditable.Text = (string?)e.NewValue;
                 }

+ 118 - 0
src/Avalonia.Native/AvaloniaNativeTextInputMethod.cs

@@ -0,0 +1,118 @@
+using System;
+using Avalonia.Input.TextInput;
+using Avalonia.Native.Interop;
+
+namespace Avalonia.Native
+{
+    internal class AvaloniaNativeTextInputMethod : ITextInputMethodImpl, IDisposable
+    {
+        private ITextInputMethodClient _client;
+        private IAvnTextInputMethodClient _nativeClient;
+        private readonly IAvnTextInputMethod _inputMethod;
+        
+        public AvaloniaNativeTextInputMethod(IAvnWindowBase nativeWindow)
+        {
+            _inputMethod = nativeWindow.InputMethod;
+        }
+
+        public void Dispose()
+        {
+            _inputMethod.Dispose();
+            _nativeClient?.Dispose();
+        }
+
+        public void Reset()
+        {
+            _inputMethod.Reset();
+        }
+
+        public void SetClient(ITextInputMethodClient client)
+        {
+            if (_client is { SupportsSurroundingText: true })
+            {
+                _client.SurroundingTextChanged -= OnSurroundingTextChanged;
+                _client.CursorRectangleChanged -= OnCursorRectangleChanged;
+                
+                _nativeClient?.Dispose();
+            }
+            
+            _nativeClient = null;
+            _client = client;
+            
+            if (client != null)
+            {
+                _nativeClient = new AvnTextInputMethodClient(client);
+
+                OnSurroundingTextChanged(this, EventArgs.Empty);
+                OnCursorRectangleChanged(this, EventArgs.Empty);
+
+                _client.SurroundingTextChanged += OnSurroundingTextChanged;
+                _client.CursorRectangleChanged += OnCursorRectangleChanged;
+            }
+
+            _inputMethod.SetClient(_nativeClient);
+        }
+
+        private void OnCursorRectangleChanged(object sender, EventArgs e)
+        {
+            if (_client == null)
+            {
+                return;
+            }
+
+            _inputMethod.SetCursorRect(_client.CursorRectangle.ToAvnRect());
+        }
+
+        private void OnSurroundingTextChanged(object sender, EventArgs e)
+        {
+            if (_client == null)
+            {
+                return;
+            }
+            
+            var surroundingText = _client.SurroundingText;
+
+            _inputMethod.SetSurroundingText(
+                surroundingText.Text,
+                surroundingText.AnchorOffset,
+                surroundingText.CursorOffset
+            );
+        }
+
+        public void SetCursorRect(Rect rect)
+        {
+            _inputMethod.SetCursorRect(rect.ToAvnRect());
+        }
+
+        public void SetOptions(TextInputOptions options)
+        {
+           
+        }
+
+        private class AvnTextInputMethodClient : NativeCallbackBase, IAvnTextInputMethodClient
+        {
+            private readonly ITextInputMethodClient _client;
+
+            public AvnTextInputMethodClient(ITextInputMethodClient client)
+            {
+                _client = client;
+            }
+
+            public void SetPreeditText(string preeditText)
+            {
+                if (_client.SupportsPreedit)
+                {
+                    _client.SetPreeditText(preeditText);
+                }
+            }
+
+            public void SelectInSurroundingText(int start, int end)
+            {
+                if (_client.SupportsSurroundingText)
+                {
+                    _client.SelectInSurroundingText(start, end);
+                }
+            }
+        }
+    }
+}

+ 10 - 1
src/Avalonia.Native/WindowImpl.cs

@@ -4,6 +4,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
+using Avalonia.Input.TextInput;
 using Avalonia.Native.Interop;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
@@ -19,6 +20,7 @@ namespace Avalonia.Native
         private double _extendTitleBarHeight = -1;
         private DoubleClickHelper _doubleClickHelper;
         private readonly ITopLevelNativeMenuExporter _nativeMenuExporter;
+        private readonly AvaloniaNativeTextInputMethod _inputMethod;
 
         internal WindowImpl(IAvaloniaNativeFactory factory, AvaloniaNativePlatformOptions opts,
             AvaloniaNativeGlPlatformGraphics glFeature) : base(factory, opts, glFeature)
@@ -33,6 +35,8 @@ namespace Avalonia.Native
             }
 
             _nativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory);
+            
+            _inputMethod = new AvaloniaNativeTextInputMethod(_native);
         }
 
         class WindowEvents : WindowBaseEvents, IAvnWindowEvents
@@ -67,7 +71,7 @@ namespace Avalonia.Native
             }
         }
 
-        public IAvnWindow Native => _native;
+        public new IAvnWindow Native => _native;
 
         public void CanResize(bool value)
         {
@@ -229,6 +233,11 @@ namespace Avalonia.Native
 
         public override object TryGetFeature(Type featureType)
         {
+            if(featureType == typeof(ITextInputMethodImpl))
+            {
+                return _inputMethod;
+            } 
+            
             if (featureType == typeof(ITopLevelNativeMenuExporter))
             {
                 return _nativeMenuExporter;

+ 17 - 0
src/Avalonia.Native/avn.idl

@@ -546,6 +546,7 @@ interface IAvnWindowBase : IUnknown
                                               IAvnClipboard* clipboard, IAvnDndResultCallback* cb, [intptr]void* sourceHandle);
      HRESULT SetTransparencyMode(AvnWindowTransparencyMode mode);
      HRESULT SetFrameThemeVariant(AvnPlatformThemeVariant mode);
+     HRESULT GetInputMethod(IAvnTextInputMethod **ppv);
 }
 
 [uuid(83e588f3-6981-4e48-9ea0-e1e569f79a91), cpp-virtual-inherits]
@@ -612,6 +613,22 @@ interface IAvnWindowEvents : IAvnWindowBaseEvents
      void GotInputWhenDisabled();
 }
 
+[uuid(f2079145-a2d9-42b8-a85e-2732e3c2b055)]
+interface IAvnTextInputMethodClient : IUnknown
+{
+    void SetPreeditText(char* preeditText);
+    void SelectInSurroundingText(int start, int length);
+}
+
+[uuid(1382a29f-e260-4c7a-b83f-c99fc72e27c2)]
+interface IAvnTextInputMethod : IUnknown
+{
+    HRESULT SetClient(IAvnTextInputMethodClient* client);
+    void Reset();
+    void SetCursorRect(AvnRect rect);
+    void SetSurroundingText(char* text, int anchorOffset, int cursorOffset);
+}
+
 [uuid(e34ae0f8-18b4-48a3-b09d-2e6b19a3cf5e)]
 interface IAvnMacOptions : IUnknown
 {

+ 12 - 10
src/Windows/Avalonia.Win32/Input/Imm32InputMethod.cs

@@ -31,6 +31,8 @@ namespace Avalonia.Win32.Input
 
         public bool ShowCompositionWindow => false;
 
+        public string? Composition { get; internal set; }
+
         public void CreateCaret()
         {
             _caretManager.TryCreate(Hwnd);
@@ -269,28 +271,28 @@ namespace Avalonia.Win32.Input
             // we're skipping this. not usable on windows
         }
 
-        public void CompositionChanged()
+        public void CompositionChanged(string? composition)
         {
-            if (!IsComposing)
-            {
-                return;
-            }
+            Composition = composition;
 
-            if(!IsActive || !Client.SupportsPreedit)
+            if (!IsActive || !Client.SupportsPreedit)
             {
                 return;
             }
 
-            var composition = GetCompositionString();
-
             Client.SetPreeditText(composition);
         }
         
-        private string? GetCompositionString()
+        public string? GetCompositionString(GCS flag)
         {
+            if (!IsComposing)
+            {
+                return null;
+            }
+
             var himc = ImmGetContext(Hwnd);
 
-            return ImmGetCompositionString(himc, GCS.GCS_COMPSTR);
+            return ImmGetCompositionString(himc, flag);
         }
 
         ~Imm32InputMethod()

+ 69 - 6
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Runtime.InteropServices;
 using Avalonia.Automation.Peers;
@@ -10,6 +11,7 @@ using Avalonia.Platform;
 using Avalonia.Threading;
 using Avalonia.Win32.Automation;
 using Avalonia.Win32.Input;
+using Avalonia.Win32.Interop;
 using Avalonia.Win32.Interop.Automation;
 using static Avalonia.Win32.Interop.UnmanagedMethods;
 
@@ -181,11 +183,17 @@ namespace Avalonia.Win32
                     }
                 case WindowsMessage.WM_CHAR:
                     {
+                        if (Imm32InputMethod.Current.IsComposing)
+                        {
+                            break;
+                        }
+
                         // Ignore control chars and chars that were handled in WM_KEYDOWN.
                         if (ToInt32(wParam) >= 32 && !_ignoreWmChar)
                         {
-                            e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner,
-                                new string((char)ToInt32(wParam), 1));
+                            var text = new string((char)ToInt32(wParam), 1);
+
+                            e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, text);
                         }
 
                         break;
@@ -709,25 +717,80 @@ namespace Avalonia.Win32
                     }
                 case WindowsMessage.WM_IME_COMPOSITION:
                     {
-                        Imm32InputMethod.Current.CompositionChanged();
+                        var previousComposition = Imm32InputMethod.Current.Composition;
+
+                        var flags = (GCS)ToInt32(lParam);
+
+                        var currentComposition = Imm32InputMethod.Current.GetCompositionString(GCS.GCS_COMPSTR);
+
+                        Imm32InputMethod.Current.CompositionChanged(currentComposition);
+
+                        switch (flags)
+                        {
+                            case GCS.GCS_RESULTSTR:                          
+                                {
+                                    if(ToInt32(wParam) >= 32)
+                                    {
+                                        Imm32InputMethod.Current.Composition = previousComposition;
+
+                                        _ignoreWmChar = true;
+                                    }
+                                    break;
+                                }
+                            case GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE:
+                                {
+                                    // Chinese IME sends WM_CHAR after composition has finished.
+                                    break;
+                                }
+                            case GCS.GCS_RESULTREADSTR | GCS.GCS_RESULTREADCLAUSE | GCS.GCS_RESULTSTR | GCS.GCS_RESULTCLAUSE:
+                                {
+                                    // Japanese IME sends WM_CHAR after composition has finished.
+                                    break;
+                                }
+                        }
 
                         break;
                     }
+                case WindowsMessage.WM_IME_SELECT:
+                    break;
                 case WindowsMessage.WM_IME_CHAR:
                 case WindowsMessage.WM_IME_COMPOSITIONFULL:
                 case WindowsMessage.WM_IME_CONTROL:
                 case WindowsMessage.WM_IME_KEYDOWN:
                 case WindowsMessage.WM_IME_KEYUP:
                 case WindowsMessage.WM_IME_NOTIFY:
-                case WindowsMessage.WM_IME_SELECT:
                     break;
                 case WindowsMessage.WM_IME_STARTCOMPOSITION:
+                    Imm32InputMethod.Current.Composition = null;
+
+                    if (Imm32InputMethod.Current.IsActive)
+                    {
+                        Imm32InputMethod.Current.Client.SetPreeditText(null);
+                    }
+
                     Imm32InputMethod.Current.IsComposing = true;
                     return IntPtr.Zero;
                 case WindowsMessage.WM_IME_ENDCOMPOSITION:
-                    Imm32InputMethod.Current.IsComposing = false;
-                    break;
+                    {
+                        var currentComposition = Imm32InputMethod.Current.Composition;
+ 
+                        //In case composition has not been comitted yet we need to do that here.
+                        if (!string.IsNullOrEmpty(currentComposition))
+                        {
+                            e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, Owner, currentComposition);
+                        }
 
+                        //Cleanup composition state.
+                        Imm32InputMethod.Current.IsComposing = false;
+                        Imm32InputMethod.Current.Composition = null;
+
+                        if (Imm32InputMethod.Current.IsActive)
+                        {
+                            Imm32InputMethod.Current.Client.SetPreeditText(null);
+                        }
+
+                        break;
+                    }
                 case WindowsMessage.WM_GETOBJECT:
                     if ((long)lParam == uiaRootObjectId && UiaCoreTypesApi.IsNetComInteropAvailable && _owner is Control control)
                     {

+ 15 - 0
tests/Avalonia.Benchmarks/Properties/launchSettings.json

@@ -0,0 +1,15 @@
+{
+  "profiles": {
+    "Avalonia.Benchmarks": {
+      "commandName": "Project"
+    },
+    "Avalonia.Benchmarks (in-process)": {
+      "commandName": "Project",
+      "commandLineArgs": "--inprocess"
+    },
+    "Avalonia.Benchmarks (debug)": {
+      "commandName": "Project",
+      "commandLineArgs": "--debug"
+    }
+  }
+}

+ 47 - 1
tests/Avalonia.Benchmarks/Styling/SelectorBenchmark.cs

@@ -1,4 +1,5 @@
-using Avalonia.Controls;
+using System;
+using Avalonia.Controls;
 using Avalonia.Styling;
 using BenchmarkDotNet.Attributes;
 
@@ -11,6 +12,8 @@ namespace Avalonia.Benchmarks.Styling
         private readonly Calendar _matchingControl;
         private readonly Selector _isCalendarSelector;
         private readonly Selector _classSelector;
+        private readonly Selector _orSelectorTwo;
+        private readonly Selector _orSelectorFive;
 
         public SelectorBenchmark()
         {
@@ -23,6 +26,14 @@ namespace Avalonia.Benchmarks.Styling
 
             _isCalendarSelector = Selectors.Is<Calendar>(null);
             _classSelector = Selectors.Class(null, className);
+
+            _orSelectorTwo = Selectors.Or(new AlwaysMatchSelector(), new AlwaysMatchSelector());
+            _orSelectorFive = Selectors.Or(
+                new AlwaysMatchSelector(), 
+                new AlwaysMatchSelector(),
+                new AlwaysMatchSelector(),
+                new AlwaysMatchSelector(),
+                new AlwaysMatchSelector());
         }
 
         [Benchmark]
@@ -48,5 +59,40 @@ namespace Avalonia.Benchmarks.Styling
         {
             return _classSelector.Match(_matchingControl);
         }
+
+        [Benchmark]
+        public SelectorMatch OrSelector_One_Match()
+        {
+            return _orSelectorTwo.Match(_matchingControl);
+        }
+
+        [Benchmark]
+        public SelectorMatch OrSelector_Five_Match()
+        {
+            return _orSelectorFive.Match(_matchingControl);
+        }
+    }
+
+    internal class AlwaysMatchSelector : Selector
+    {
+        public override bool InTemplate => false;
+
+        public override bool IsCombinator => false;
+
+        public override Type TargetType => null;
+
+        public override string ToString(Style owner)
+        {
+            return "Always";
+        }
+
+        protected override SelectorMatch Evaluate(StyledElement control, IStyle parent, bool subscribe)
+        {
+            return SelectorMatch.AlwaysThisType;
+        }
+
+        protected override Selector MovePrevious() => null;
+
+        protected override Selector MovePreviousOrParent() => null;
     }
 }

+ 19 - 0
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@@ -1058,6 +1058,25 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Should_Move_Caret_To_EndOfLine()
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                var tb = new TextBox
+                {
+                    Template = CreateTemplate(),
+                    Text = "AB\nAB"
+                };
+
+                tb.Measure(Size.Infinity);
+
+                RaiseKeyEvent(tb, Key.End, KeyModifiers.Shift);
+
+                Assert.Equal(2, tb.CaretIndex);
+            }
+        }
+
         private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
             focusManager: new FocusManager(),
             keyboardDevice: () => new KeyboardDevice(),