Browse Source

Merge branch 'master' into feature/SharedSizeScope

Steven Kirk 7 years ago
parent
commit
39d7907022
44 changed files with 748 additions and 96 deletions
  1. 3 0
      azure-pipelines.yml
  2. 1 0
      build.cake
  3. 1 1
      build/SharedVersion.props
  4. 9 3
      native/Avalonia.Native/inc/avalonia-native.h
  5. 6 0
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  6. 2 2
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme
  7. 14 0
      native/Avalonia.Native/src/OSX/AvnString.h
  8. 55 0
      native/Avalonia.Native/src/OSX/AvnString.mm
  9. 12 7
      native/Avalonia.Native/src/OSX/clipboard.mm
  10. 7 3
      native/Avalonia.Native/src/OSX/window.mm
  11. 6 1
      packages/Avalonia/Avalonia.csproj
  12. 6 0
      packages/Avalonia/Avalonia.props
  13. 13 4
      parameters.cake
  14. 6 13
      samples/ControlCatalog/App.xaml
  15. 1 1
      samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml
  16. 31 17
      src/Avalonia.Animation/AnimationInstance`1.cs
  17. 6 1
      src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
  18. 8 3
      src/Avalonia.Controls/Button.cs
  19. 1 1
      src/Avalonia.Controls/DropDown.cs
  20. 1 1
      src/Avalonia.Controls/Primitives/AdornerLayer.cs
  21. 14 0
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  22. 4 4
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  23. 15 0
      src/Avalonia.Controls/ScrollViewer.cs
  24. 3 0
      src/Avalonia.Controls/TextBox.cs
  25. 1 1
      src/Avalonia.Controls/UserControl.cs
  26. 1 0
      src/Avalonia.Native/.gitignore
  27. 2 0
      src/Avalonia.Native/Avalonia.Native.csproj
  28. 1 0
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  29. 11 5
      src/Avalonia.Native/ClipboardImpl.cs
  30. 5 1
      src/Avalonia.Native/WindowImpl.cs
  31. 56 0
      src/Avalonia.Themes.Default/Accents/BaseDark.xaml
  32. 1 0
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  33. 16 8
      src/Avalonia.Themes.Default/ScrollBar.xaml
  34. 4 2
      src/Avalonia.Themes.Default/ScrollViewer.xaml
  35. 15 0
      src/Avalonia.Themes.Default/UserControl.xaml
  36. 2 1
      src/Avalonia.Themes.Default/Window.xaml
  37. 15 1
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  38. 19 4
      src/Avalonia.Visuals/Visual.cs
  39. 1 1
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  40. 23 6
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs
  41. 206 2
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  42. 2 2
      tests/Avalonia.Controls.UnitTests/UserControlTests.cs
  43. 98 0
      tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs
  44. 44 0
      tests/Avalonia.Visuals.UnitTests/VisualTests.cs

+ 3 - 0
azure-pipelines.yml

@@ -29,6 +29,7 @@ jobs:
     inputs:
       testResultsFormat: 'VSTest'
       testResultsFiles: '$(Build.SourcesDirectory)/artifacts/test-results/*.trx'
+    condition: not(canceled())
      
 - job: macOS
   pool:
@@ -77,6 +78,7 @@ jobs:
     inputs:
       testResultsFormat: 'VSTest'
       testResultsFiles: '$(Build.SourcesDirectory)/artifacts/test-results/*.trx'
+    condition: not(canceled())
   
   - task: PublishBuildArtifacts@1
     inputs:
@@ -111,6 +113,7 @@ jobs:
     inputs:
       testResultsFormat: 'VSTest'
       testResultsFiles: '$(Build.SourcesDirectory)/artifacts/test-results/*.trx'
+    condition: not(canceled())
 
   - task: PublishBuildArtifacts@1
     inputs:

+ 1 - 0
build.cake

@@ -50,6 +50,7 @@ Setup<Parameters>(context =>
     Information("IsPullRequest: " + parameters.IsPullRequest);
     Information("IsMainRepo: " + parameters.IsMainRepo);
     Information("IsMasterBranch: " + parameters.IsMasterBranch);
+    Information("IsReleaseBranch: " + parameters.IsReleaseBranch);
     Information("IsTagged: " + parameters.IsTagged);
     Information("IsReleasable: " + parameters.IsReleasable);
     Information("IsMyGetRelease: " + parameters.IsMyGetRelease);

+ 1 - 1
build/SharedVersion.props

@@ -2,7 +2,7 @@
   xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup>
     <Product>Avalonia</Product>
-    <Version>0.7.0</Version>
+    <Version>0.7.1</Version>
     <Copyright>Copyright 2018 &#169; The AvaloniaUI Project</Copyright>
     <PackageLicenseUrl>https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md</PackageLicenseUrl>
     <PackageProjectUrl>https://github.com/AvaloniaUI/Avalonia/</PackageProjectUrl>

+ 9 - 3
native/Avalonia.Native/inc/avalonia-native.h

@@ -173,6 +173,12 @@ public:
     virtual HRESULT ObtainGlFeature(IAvnGlFeature** ppv) = 0;
 };
 
+AVNCOM(IAvnString, 17) : IUnknown
+{
+    virtual HRESULT Pointer(void**retOut) = 0;
+    virtual HRESULT Length(int*ret) = 0;
+};
+
 AVNCOM(IAvnWindowBase, 02) : IUnknown
 {
     virtual HRESULT Show() = 0;
@@ -210,7 +216,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
     virtual HRESULT ShowDialog (IUnknown**ppv) = 0;
     virtual HRESULT SetCanResize(bool value) = 0;
     virtual HRESULT SetHasDecorations(bool value) = 0;
-    virtual HRESULT SetTitle (const char* title) = 0;
+    virtual HRESULT SetTitle (void* utf8Title) = 0;
     virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
     virtual HRESULT SetWindowState(AvnWindowState state) = 0;
     virtual HRESULT GetWindowState(AvnWindowState*ret) = 0;
@@ -315,8 +321,8 @@ AVNCOM(IAvnScreens, 0e) : IUnknown
 
 AVNCOM(IAvnClipboard, 0f) : IUnknown
 {
-    virtual HRESULT GetText (void** retOut) = 0;
-    virtual HRESULT SetText (char* text) = 0;
+    virtual HRESULT GetText (IAvnString**ppv) = 0;
+    virtual HRESULT SetText (void* utf8Text) = 0;
     virtual HRESULT Clear() = 0;
 };
 

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

@@ -9,6 +9,7 @@
 /* Begin PBXBuildFile section */
 		37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
 		37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; };
+		37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; };
 		37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; };
 		5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
 		5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
@@ -26,6 +27,8 @@
 		37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = "<group>"; };
 		37C09D8721580FE4006A6758 /* SystemDialogs.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = SystemDialogs.mm; sourceTree = "<group>"; };
 		37C09D8A21581EF2006A6758 /* window.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = window.h; sourceTree = "<group>"; };
+		37DDA9AF219330F8002E132B /* AvnString.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AvnString.mm; sourceTree = "<group>"; };
+		37DDA9B121933371002E132B /* AvnString.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AvnString.h; sourceTree = "<group>"; };
 		37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = "<group>"; };
 		5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = "<group>"; };
 		5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; };
@@ -65,6 +68,8 @@
 		AB7A61E62147C814003C5833 = {
 			isa = PBXGroup;
 			children = (
+				37DDA9B121933371002E132B /* AvnString.h */,
+				37DDA9AF219330F8002E132B /* AvnString.mm */,
 				37A4E71A2178846A00EACBCD /* headers */,
 				AB573DC3217605E400D389A2 /* gl.mm */,
 				5BF943652167AD1D009CAE35 /* cursor.h */,
@@ -161,6 +166,7 @@
 			files = (
 				5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */,
 				5B21A982216530F500CEE36E /* cursor.mm in Sources */,
+				37DDA9B0219330F8002E132B /* AvnString.mm in Sources */,
 				AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
 				37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
 				37A517B32159597E00FBA241 /* Screens.mm in Sources */,

+ 2 - 2
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme

@@ -38,7 +38,7 @@
       selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
       launchStyle = "0"
       useCustomWorkingDirectory = "YES"
-      customWorkingDirectory = "$PROJECT_DIR/../../../../samples/ControlCatalog"
+      customWorkingDirectory = "$PROJECT_DIR/../../../../samples/ControlCatalog.NetCore"
       ignoresPersistentStateOnLaunch = "NO"
       debugDocumentVersioning = "YES"
       debugServiceExtension = "internal"
@@ -58,7 +58,7 @@
       </MacroExpansion>
       <CommandLineArguments>
          <CommandLineArgument
-            argument = "bin/Debug/netcoreapp2.0/ControlCatalog.dll"
+            argument = "bin/Debug/netcoreapp2.0/ControlCatalog.NetCore.dll"
             isEnabled = "YES">
          </CommandLineArgument>
       </CommandLineArguments>

+ 14 - 0
native/Avalonia.Native/src/OSX/AvnString.h

@@ -0,0 +1,14 @@
+//
+//  AvnString.h
+//  Avalonia.Native.OSX
+//
+//  Created by Dan Walmsley on 07/11/2018.
+//  Copyright © 2018 Avalonia. All rights reserved.
+//
+
+#ifndef AvnString_h
+#define AvnString_h
+
+extern IAvnString* CreateAvnString(NSString* string);
+
+#endif /* AvnString_h */

+ 55 - 0
native/Avalonia.Native/src/OSX/AvnString.mm

@@ -0,0 +1,55 @@
+//
+//  AvnString.m
+//  Avalonia.Native.OSX
+//
+//  Created by Dan Walmsley on 07/11/2018.
+//  Copyright © 2018 Avalonia. All rights reserved.
+//
+
+#include "common.h"
+
+class AvnStringImpl : public virtual ComSingleObject<IAvnString, &IID_IAvnString>
+{
+private:
+    NSString* _string;
+    
+public:
+    FORWARD_IUNKNOWN()
+    
+    AvnStringImpl(NSString* string)
+    {
+        _string = string;
+    }
+    
+    virtual HRESULT Pointer(void**retOut) override
+    {
+        @autoreleasepool
+        {
+            if(retOut == nullptr)
+            {
+                return E_POINTER;
+            }
+            
+            *retOut = (void*)_string.UTF8String;
+            
+            return S_OK;
+        }
+    }
+    
+    virtual HRESULT Length(int*retOut) override
+    {
+        if(retOut == nullptr)
+        {
+            return E_POINTER;
+        }
+        
+        *retOut = (int)_string.length;
+        
+        return S_OK;
+    }
+};
+
+IAvnString* CreateAvnString(NSString* string)
+{
+    return new AvnStringImpl(string);
+}

+ 12 - 7
native/Avalonia.Native/src/OSX/clipboard.mm

@@ -2,29 +2,34 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 #include "common.h"
+#include "AvnString.h"
 
 class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard>
 {
 public:
     FORWARD_IUNKNOWN()
-    virtual HRESULT GetText (void** retOut) override
+    virtual HRESULT GetText (IAvnString**ppv) override
     {
         @autoreleasepool
         {
-            NSString *str = [[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString];
-            *retOut = (void *)str.UTF8String;
+            if(ppv == nullptr)
+            {
+                return E_POINTER;
+            }
+            
+            *ppv = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]);
+            
+            return S_OK;
         }
-        
-        return S_OK;
     }
     
-    virtual HRESULT SetText (char* text) override
+    virtual HRESULT SetText (void* utf8String) override
     {
         @autoreleasepool
         {
             NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
             [pasteBoard clearContents];
-            [pasteBoard setString:@(text) forType:NSPasteboardTypeString];
+            [pasteBoard setString:[NSString stringWithUTF8String:(const char*)utf8String] forType:NSPasteboardTypeString];
         }
         
         return S_OK;

+ 7 - 3
native/Avalonia.Native/src/OSX/window.mm

@@ -530,11 +530,11 @@ private:
         }
     }
     
-    virtual HRESULT SetTitle (const char* title) override
+    virtual HRESULT SetTitle (void* utf8title) override
     {
         @autoreleasepool
         {
-            _lastTitle = [NSString stringWithUTF8String:title];
+            _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title];
             [Window setTitle:_lastTitle];
             [Window setTitleVisibility:NSWindowTitleVisible];
             
@@ -963,7 +963,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
 
 - (BOOL)performKeyEquivalent:(NSEvent *)event
 {
-    return _lastKeyHandled;
+    bool result = _lastKeyHandled;
+    
+    _lastKeyHandled = false;
+    
+    return result;
 }
 
 - (void)keyDown:(NSEvent *)event

+ 6 - 1
packages/Avalonia/Avalonia.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="AggregatePackage.NuGet.Sdk">
+<Project Sdk="AggregatePackage.NuGet.Sdk">
   <PropertyGroup>
       <TargetFrameworks>netstandard2.0;net461;netcoreapp2.0</TargetFrameworks>
   </PropertyGroup>
@@ -27,6 +27,11 @@
         <Visible>false</Visible>
         <BuildAction>None</BuildAction>
       </_PackageFiles>
+      <_PackageFiles Include="Avalonia.props">
+        <PackagePath>build/Avalonia.props</PackagePath>
+        <Visible>false</Visible>
+        <BuildAction>None</BuildAction>
+      </_PackageFiles>
     </ItemGroup>
   </Target>
 

+ 6 - 0
packages/Avalonia/Avalonia.props

@@ -0,0 +1,6 @@
+<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup>
+    <AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
+    <AvaloniaPreviewerNetFullToolPath>$(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe</AvaloniaPreviewerNetFullToolPath>
+  </PropertyGroup>
+</Project>

+ 13 - 4
parameters.cake

@@ -9,6 +9,7 @@ public class Parameters
     public string MasterBranch { get; private set; }
     public string ReleasePlatform { get; private set; }
     public string ReleaseConfiguration { get; private set; }
+    public string ReleaseBranchPrefix { get; private set; }
     public string MSBuildSolution { get; private set; }
     public bool IsLocalBuild { get; private set; }
     public bool IsRunningOnUnix { get; private set; }
@@ -18,6 +19,7 @@ public class Parameters
     public bool IsPullRequest { get; private set; }
     public bool IsMainRepo { get; private set; }
     public bool IsMasterBranch { get; private set; }
+    public bool IsReleaseBranch { get; private set; }
     public bool IsTagged { get; private set; }
     public bool IsReleasable { get; private set; }
     public bool IsMyGetRelease { get; private set; }
@@ -46,8 +48,9 @@ public class Parameters
         SkipTests = context.HasArgument("skip-tests");
 
         // CONFIGURATION
-        MainRepo = "AvaloniaUI/Avalonia";
+        MainRepo = "https://github.com/AvaloniaUI/Avalonia";
         MasterBranch = "master";
+        ReleaseBranchPrefix = "refs/heads/release/";
         ReleaseConfiguration = "Release";
         MSBuildSolution = "./dirs.proj";
 
@@ -59,12 +62,14 @@ public class Parameters
         IsRunningOnAzure = buildSystem.IsRunningOnVSTS || buildSystem.IsRunningOnTFS || context.EnvironmentVariable("LOGNAME") == "vsts";
         
         IsPullRequest = buildSystem.AppVeyor.Environment.PullRequest.IsPullRequest;
-        IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, buildSystem.AppVeyor.Environment.Repository.Name);
-        IsMasterBranch = StringComparer.OrdinalIgnoreCase.Equals(MasterBranch, buildSystem.AppVeyor.Environment.Repository.Branch);
+        IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, context.EnvironmentVariable("BUILD_REPOSITORY_URI"));
+        IsMasterBranch = StringComparer.OrdinalIgnoreCase.Equals(MasterBranch, context.EnvironmentVariable("BUILD_SOURCEBRANCHNAME"));
+        IsReleaseBranch = (context.EnvironmentVariable("BUILD_SOURCEBRANCH")??"").StartsWith(ReleaseBranchPrefix, StringComparison.OrdinalIgnoreCase);
         IsTagged = buildSystem.AppVeyor.Environment.Repository.Tag.IsTag 
                 && !string.IsNullOrWhiteSpace(buildSystem.AppVeyor.Environment.Repository.Tag.Name);
         IsReleasable = StringComparer.OrdinalIgnoreCase.Equals(ReleaseConfiguration, Configuration);
         IsMyGetRelease = !IsTagged && IsReleasable;
+        IsNuGetRelease = IsMainRepo && IsReleasable && IsReleaseBranch;
 
         // VERSION
         Version = context.Argument("force-nuget-version", GetVersion());
@@ -92,9 +97,13 @@ public class Parameters
         }
         else if (IsRunningOnAzure)
         {
+            if(!IsNuGetRelease)
+            {
                 // Use AssemblyVersion with Build as version
                 Version += "-build" + context.EnvironmentVariable("BUILD_BUILDID") + "-beta";
-                PublishTestResults = true; 
+            }
+
+            PublishTestResults = true; 
         }
 
         // DIRECTORIES

+ 6 - 13
samples/ControlCatalog/App.xaml

@@ -2,23 +2,16 @@
   <Application.Styles>
     <StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/>
     <StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/>
-
     <Style Selector="TextBlock.h1">
-      <Setter Property="Foreground" Value="#212121"/>
-      <Setter Property="FontSize" Value="20"/>
+      <Setter Property="FontSize" Value="{DynamicResource FontSizeLarge}"/>
       <Setter Property="FontWeight" Value="Medium"/>
     </Style>
-
     <Style Selector="TextBlock.h2">
-      <Setter Property="Foreground" Value="#727272"/>
-      <Setter Property="FontSize" Value="13"/>
+      <Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
     </Style>
-
-      <Style Selector="TextBlock.h3">
-          <Setter Property="Foreground" Value="#a2a2a2"/>
-          <Setter Property="FontSize" Value="13"/>
-      </Style>
-
-      <StyleInclude Source="resm:ControlCatalog.SideBar.xaml"/>
+    <Style Selector="TextBlock.h3">
+      <Setter Property="FontSize" Value="{DynamicResource FontSizeSmall}"/>
+    </Style>
+    <StyleInclude Source="resm:ControlCatalog.SideBar.xaml"/>
   </Application.Styles>
 </Application>

+ 1 - 1
samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml

@@ -19,7 +19,7 @@
         <LayoutTransformControl.LayoutTransform>
           <RotateTransform Angle="{Binding #rotation.Value}"/>
         </LayoutTransformControl.LayoutTransform>
-        <TextBlock>Layout Transform</TextBlock>
+          <Button Background="White">Layout Transform</Button>
       </LayoutTransformControl>
     </Grid>
   </DockPanel>

+ 31 - 17
src/Avalonia.Animation/AnimationInstance`1.cs

@@ -72,10 +72,13 @@ namespace Avalonia.Animation
             _onCompleteAction = OnComplete;
             _interpolator = Interpolator;
             _baseClock = baseClock;
-        }
+          }
 
         protected override void Unsubscribed()
         {
+            //Animation may have been stopped before it has finished
+            ApplyFinalFill();
+
             _timerSubscription?.Dispose();
             _clock.PlayState = PlayState.Stop;
         }
@@ -98,11 +101,15 @@ namespace Avalonia.Animation
             }
         }
 
-        private void DoComplete()
+        private void ApplyFinalFill()
         {
             if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both)
                 _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
+        }
 
+        private void DoComplete()
+        {
+            ApplyFinalFill();
             _onCompleteAction?.Invoke();
             PublishCompleted();
         }
@@ -133,6 +140,7 @@ namespace Avalonia.Animation
             DoPlayStates();
             var delayEndpoint = _delay;
             var iterationEndpoint = delayEndpoint + _duration;
+            var iterationTime = time;
 
             //determine if time is currently in the first iteration.
             if (time >= TimeSpan.Zero & time <= iterationEndpoint)
@@ -142,7 +150,7 @@ namespace Avalonia.Animation
             else if (time > iterationEndpoint)
             {
                 //Subtract first iteration to properly get the subsequent iteration time
-                time -= iterationEndpoint;
+                iterationTime -= iterationEndpoint;
 
                 if (!_iterationDelay & delayEndpoint > TimeSpan.Zero)
                 {
@@ -151,39 +159,45 @@ namespace Avalonia.Animation
                 }
 
                 //Calculate the current iteration number
-                _currentIteration = (int)Math.Floor((double)((double)time.Ticks / iterationEndpoint.Ticks)) + 2;
+                _currentIteration = Math.Min(_repeatCount,(int)Math.Floor((double)((double)iterationTime.Ticks / iterationEndpoint.Ticks)) + 2);
             }
             else
             {
                 return;
             }
 
-            time = TimeSpan.FromTicks((long)(time.Ticks % iterationEndpoint.Ticks));
-
-            if (!_isLooping)
-            {
-                if ((_currentIteration > _repeatCount) || (time > iterationEndpoint))
-                    DoComplete();
-            }
-
-            // Determine if the current iteration should have its normalized time inverted.
+             // Determine if the current iteration should have its normalized time inverted.
             bool isCurIterReverse = _animationDirection == PlaybackDirection.Normal ? false :
                                     _animationDirection == PlaybackDirection.Alternate ? (_currentIteration % 2 == 0) ? false : true :
                                     _animationDirection == PlaybackDirection.AlternateReverse ? (_currentIteration % 2 == 0) ? true : false :
                                     _animationDirection == PlaybackDirection.Reverse ? true : false;
-
-            if (delayEndpoint > TimeSpan.Zero & time < delayEndpoint)
+   
+            if (!_isLooping)
+            {
+                var totalTime = _iterationDelay ? _repeatCount * ( _duration.Ticks + _delay.Ticks) : _repeatCount * _duration.Ticks + _delay.Ticks;
+                if (time.Ticks >= totalTime)
+                {
+                    var easedTime = _easeFunc.Ease(isCurIterReverse ? 0.0 : 1.0);
+                    _lastInterpValue = _interpolator(easedTime, _neutralValue);
+                   
+                    DoComplete();
+                    return;
+                }
+            }
+            iterationTime = TimeSpan.FromTicks((long)(iterationTime.Ticks % iterationEndpoint.Ticks));
+        
+            if (delayEndpoint > TimeSpan.Zero & iterationTime < delayEndpoint)
             {
                 DoDelay();
             }
             else
             {
                 // Offset the delay time            
-                time -= delayEndpoint;
+                iterationTime -= delayEndpoint;
                 iterationEndpoint -= delayEndpoint;
 
                 // Normalize time
-                var interpVal = (double)time.Ticks / iterationEndpoint.Ticks;
+                var interpVal = (double)iterationTime.Ticks / iterationEndpoint.Ticks;
 
                 if (isCurIterReverse)
                     interpVal = 1 - interpVal;

+ 6 - 1
src/Avalonia.Base/Collections/AvaloniaListExtensions.cs

@@ -104,7 +104,12 @@ namespace Avalonia.Collections
                     case NotifyCollectionChangedAction.Move:
                     case NotifyCollectionChangedAction.Replace:
                         Remove(e.OldStartingIndex, e.OldItems);
-                        Add(e.NewStartingIndex, e.NewItems);
+                        int newIndex = e.NewStartingIndex;
+                        if(newIndex > e.OldStartingIndex)
+                        {
+                            newIndex -= e.OldItems.Count;
+                        }
+                        Add(newIndex, e.NewItems);
                         break;
 
                     case NotifyCollectionChangedAction.Remove:

+ 8 - 3
src/Avalonia.Controls/Button.cs

@@ -2,10 +2,12 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Linq;
 using System.Windows.Input;
 using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -251,7 +253,10 @@ namespace Avalonia.Controls
                 IsPressed = false;
                 e.Handled = true;
 
-                if (ClickMode == ClickMode.Release && new Rect(Bounds.Size).Contains(e.GetPosition(this)))
+                var hittest = this.GetVisualsAt(e.GetPosition(this));
+
+                if (ClickMode == ClickMode.Release &&
+                    hittest.Any(c => c == this || (c as IStyledElement)?.TemplatedParent == this))
                 {
                     OnClick();
                 }
@@ -261,9 +266,9 @@ namespace Avalonia.Controls
         protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
         {
             base.UpdateDataValidation(property, status);
-            if(property == CommandProperty)
+            if (property == CommandProperty)
             {
-                if(status?.ErrorType == BindingErrorType.Error)
+                if (status?.ErrorType == BindingErrorType.Error)
                 {
                     IsEnabled = false;
                 }

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

@@ -145,7 +145,7 @@ namespace Avalonia.Controls
         {
             if (!e.Handled)
             {
-                if (((IVisual)e.Source).GetVisualRoot() is PopupRoot)
+                if (_popup?.PopupRoot != null && ((IVisual)e.Source).GetVisualRoot() == _popup?.PopupRoot)
                 {
                     if (UpdateSelectionFromEventSource(e.Source))
                     {

+ 1 - 1
src/Avalonia.Controls/Primitives/AdornerLayer.cs

@@ -89,7 +89,7 @@ namespace Avalonia.Controls.Primitives
                 control.Clip = clip;
             }
 
-            clip.Rect = bounds.Clip.TransformToAABB(-bounds.Transform);
+            clip.Rect = bounds.Bounds;
         }
 
         private void ChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)

+ 14 - 0
src/Avalonia.Controls/Primitives/ScrollBar.cs

@@ -128,6 +128,20 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            if (e.Key == Key.PageUp)
+            {
+                LargeDecrement();
+                e.Handled = true;
+            }
+            else if (e.Key == Key.PageDown)
+            {
+                LargeIncrement();
+                e.Handled = true;
+            }
+        }
+
         protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
         {
             base.OnTemplateApplied(e);

+ 4 - 4
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@@ -211,10 +211,10 @@ namespace Avalonia.Controls.Remote.Server
                 }
                 if(obj is KeyEventMessage key)
                 {
-                    Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
-
                     Dispatcher.UIThread.Post(() =>
                     {
+                        Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
+
                         Input?.Invoke(new RawKeyEventArgs(
                             KeyboardDevice,
                             0,
@@ -225,10 +225,10 @@ namespace Avalonia.Controls.Remote.Server
                 }
                 if(obj is TextInputEventMessage text)
                 {
-                    Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
-
                     Dispatcher.UIThread.Post(() =>
                     {
+                        Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
+
                         Input?.Invoke(new RawTextInputEventArgs(
                             KeyboardDevice,
                             0,

+ 15 - 0
src/Avalonia.Controls/ScrollViewer.cs

@@ -4,6 +4,7 @@
 using System;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
+using Avalonia.Input;
 
 namespace Avalonia.Controls
 {
@@ -441,5 +442,19 @@ namespace Avalonia.Controls
             RaisePropertyChanged(VerticalScrollBarValueProperty, 0, VerticalScrollBarValue);
             RaisePropertyChanged(VerticalScrollBarViewportSizeProperty, 0, VerticalScrollBarViewportSize);
         }
+
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            if (e.Key == Key.PageUp)
+            {
+                VerticalScrollBarValue = Math.Max(_offset.Y - _viewport.Height, 0);
+                e.Handled = true;
+            }
+            else if (e.Key == Key.PageDown)
+            {
+                VerticalScrollBarValue = Math.Min(_offset.Y + _viewport.Height, VerticalScrollBarMaximum);
+                e.Handled = true;
+            }
+        }
     }
 }

+ 3 - 0
src/Avalonia.Controls/TextBox.cs

@@ -14,6 +14,7 @@ using Avalonia.Interactivity;
 using Avalonia.Media;
 using Avalonia.Metadata;
 using Avalonia.Data;
+using Avalonia.Utilities;
 
 namespace Avalonia.Controls
 {
@@ -655,6 +656,8 @@ namespace Avalonia.Controls
             if (_presenter != null && e.Device.Captured == _presenter)
             {
                 var point = e.GetPosition(_presenter);
+
+                point = new Point(MathUtilities.Clamp(point.X, 0, _presenter.Bounds.Width - 1), MathUtilities.Clamp(point.Y, 0, _presenter.Bounds.Height - 1));
                 CaretIndex = SelectionEnd = _presenter.GetCaretIndex(point);
             }
         }

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

@@ -28,7 +28,7 @@ namespace Avalonia.Controls
         }
 
         /// <inheritdoc/>
-        Type IStyleable.StyleKey => typeof(ContentControl);
+        Type IStyleable.StyleKey => typeof(UserControl);
 
         /// <inheritdoc/>
         void INameScope.Register(string name, object element)

+ 1 - 0
src/Avalonia.Native/.gitignore

@@ -0,0 +1 @@
+Generated/*.cs

+ 2 - 0
src/Avalonia.Native/Avalonia.Native.csproj

@@ -7,6 +7,8 @@
     <CastXmlPath Condition="Exists('/usr/bin/castxml')">/usr/bin/castxml</CastXmlPath>
     <CastXmlPath Condition="Exists('/usr/local/bin/castxml')">/usr/local/bin/castxml</CastXmlPath>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
+    <!-- This is needed because Rider doesn't see generated files in obj for some reason -->
+    <SharpGenGeneratedCodeFolder>$(MSBuildThisFileDirectory)/Generated</SharpGenGeneratedCodeFolder>
   </PropertyGroup>
 
   <ItemGroup Condition="'$(Configuration)' == 'Release' AND '$([MSBuild]::IsOSPlatform(OSX))' == 'true'">

+ 1 - 0
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -76,6 +76,7 @@ namespace Avalonia.Native
                 .Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
                 .Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
                 .Bind<IWindowingPlatformGlFeature>().ToConstant(new GlPlatformFeature(_factory.ObtainGlFeature()))
+                .Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(InputModifiers.Windows))
                 .Bind<AvaloniaNativeOptions>().ToConstant(opts);
         }
 

+ 11 - 5
src/Avalonia.Native/ClipboardImpl.cs

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
 using System.Runtime.InteropServices;
 using Avalonia.Input.Platform;
 using Avalonia.Native.Interop;
+using Avalonia.Platform.Interop;
 
 namespace Avalonia.Native
 {
@@ -24,12 +25,14 @@ namespace Avalonia.Native
             return Task.CompletedTask;
         }
 
-        public Task<string> GetTextAsync()
+        public unsafe Task<string> GetTextAsync()
         {
-            var outPtr = _native.GetText();
-            var text = Marshal.PtrToStringAnsi(outPtr);
+            using (var text = _native.GetText())
+            {
+                var result = System.Text.Encoding.UTF8.GetString((byte*)text.Pointer(), text.Length());
 
-            return Task.FromResult(text);
+                return Task.FromResult(result);
+            }
         }
 
         public Task SetTextAsync(string text)
@@ -38,7 +41,10 @@ namespace Avalonia.Native
 
             if (text != null)
             {
-                _native.SetText(text);
+                using (var buffer = new Utf8Buffer(text))
+                {
+                    _native.SetText(buffer.DangerousGetHandle());
+                }
             }
 
             return Task.CompletedTask;

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

@@ -5,6 +5,7 @@ using System;
 using Avalonia.Controls;
 using Avalonia.Native.Interop;
 using Avalonia.Platform;
+using Avalonia.Platform.Interop;
 
 namespace Avalonia.Native
 {
@@ -68,7 +69,10 @@ namespace Avalonia.Native
 
         public void SetTitle(string title)
         {
-            _native.SetTitle(title);
+            using (var buffer = new Utf8Buffer(title))
+            {
+                _native.SetTitle(buffer.DangerousGetHandle());
+            }
         }
 
         public WindowState WindowState

+ 56 - 0
src/Avalonia.Themes.Default/Accents/BaseDark.xaml

@@ -0,0 +1,56 @@
+<Style xmlns="https://github.com/avaloniaui"
+       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+       xmlns:sys="clr-namespace:System;assembly=mscorlib">
+    <Style.Resources>
+
+        <Color x:Key="ThemeAccentColor">#CC119EDA</Color>
+        <Color x:Key="ThemeAccentColor2">#99119EDA</Color>
+        <Color x:Key="ThemeAccentColor3">#66119EDA</Color>
+        <Color x:Key="ThemeAccentColor4">#33119EDA</Color>
+
+        <Color x:Key="ThemeBackgroundColor">#FF282828</Color>
+        <Color x:Key="ThemeBorderLowColor">#FF505050</Color>
+        <Color x:Key="ThemeBorderMidColor">#FF808080</Color>
+        <Color x:Key="ThemeBorderHighColor">#FFA0A0A0</Color>
+        <Color x:Key="ThemeControlLowColor">#FF282828</Color>
+        <Color x:Key="ThemeControlMidColor">#FF505050</Color>
+        <Color x:Key="ThemeControlHighColor">#FF808080</Color>
+        <Color x:Key="ThemeControlHighlightLowColor">#FFA8A8A8</Color>
+        <Color x:Key="ThemeControlHighlightMidColor">#FF828282</Color>
+        <Color x:Key="ThemeControlHighlightHighColor">#FF505050</Color>
+        <Color x:Key="ThemeForegroundColor">#FFDEDEDE</Color>
+        <Color x:Key="ThemeForegroundLowColor">#FF808080</Color>
+
+        <Color x:Key="HighlightColor">#FF119EDA</Color>
+        <Color x:Key="ErrorColor">#FFFF0000</Color>
+        <Color x:Key="ErrorLowColor">#10FF0000</Color>
+
+        <SolidColorBrush x:Key="ThemeBackgroundBrush" Color="{DynamicResource ThemeBackgroundColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeBorderLowBrush" Color="{DynamicResource ThemeBorderLowColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeBorderMidBrush" Color="{DynamicResource ThemeBorderMidColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeBorderHighBrush" Color="{DynamicResource ThemeBorderHighColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeControlLowBrush" Color="{DynamicResource ThemeControlLowColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeControlMidBrush" Color="{DynamicResource ThemeControlMidColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeControlHighBrush" Color="{DynamicResource ThemeControlHighColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeControlHighlightLowBrush" Color="{DynamicResource ThemeControlHighlightLowColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeControlHighlightMidBrush" Color="{DynamicResource ThemeControlHighlightMidColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeControlHighlightHighBrush" Color="{DynamicResource ThemeControlHighlightHighColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeForegroundBrush" Color="{DynamicResource ThemeForegroundColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeForegroundLowBrush" Color="{DynamicResource ThemeForegroundLowColor}"></SolidColorBrush>
+
+        <SolidColorBrush x:Key="HighlightBrush" Color="{DynamicResource HighlightColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeAccentBrush" Color="{DynamicResource ThemeAccentColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeAccentBrush2" Color="{DynamicResource ThemeAccentColor2}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeAccentBrush3" Color="{DynamicResource ThemeAccentColor3}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ThemeAccentBrush4" Color="{DynamicResource ThemeAccentColor4}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ErrorBrush" Color="{DynamicResource ErrorColor}"></SolidColorBrush>
+        <SolidColorBrush x:Key="ErrorLowBrush" Color="{DynamicResource ErrorLowColor}"></SolidColorBrush>
+
+        <Thickness x:Key="ThemeBorderThickness">1,1,1,1</Thickness>
+        <sys:Double x:Key="ThemeDisabledOpacity">0.5</sys:Double>
+
+        <sys:Double x:Key="FontSizeSmall">10</sys:Double>
+        <sys:Double x:Key="FontSizeNormal">12</sys:Double>
+        <sys:Double x:Key="FontSizeLarge">16</sys:Double>
+    </Style.Resources>
+</Style>

+ 1 - 0
src/Avalonia.Themes.Default/DefaultTheme.xaml

@@ -35,6 +35,7 @@
   <StyleInclude Source="resm:Avalonia.Themes.Default.Expander.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.TreeView.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.TreeViewItem.xaml?assembly=Avalonia.Themes.Default"/>
+  <StyleInclude Source="resm:Avalonia.Themes.Default.UserControl.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.Window.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.EmbeddableControlRoot.xaml?assembly=Avalonia.Themes.Default"/>
   <StyleInclude Source="resm:Avalonia.Themes.Default.CalendarButton.xaml?assembly=Avalonia.Themes.Default"/>

+ 16 - 8
src/Avalonia.Themes.Default/ScrollBar.xaml

@@ -7,7 +7,8 @@
                         <RepeatButton Name="PART_LineUpButton"
                                       Classes="repeat"
                                       Grid.Row="0"
-                                      Grid.Column="0">
+                                      Grid.Column="0"
+                                      Focusable="False">
                             <Path Data="M 0,4 C0,4 0,6 0,6 0,6 3.5,2.5 3.5,2.5 3.5,2.5 7,6 7,6 7,6 7,4 7,4 7,4 3.5,0.5 3.5,0.5 3.5,0.5 0,4 0,4 z"
                                   Stretch="Uniform"
                                   Fill="{DynamicResource ThemeForegroundLowBrush}" />
@@ -21,11 +22,13 @@
                                Orientation="{TemplateBinding Orientation}">
                             <Track.DecreaseButton>
                                 <RepeatButton Name="PART_PageUpButton"
-                                              Classes="repeattrack" />
+                                              Classes="repeattrack"
+                                              Focusable="False"/>
                             </Track.DecreaseButton>
                             <Track.IncreaseButton>
                                 <RepeatButton Name="PART_PageDownButton"
-                                              Classes="repeattrack" />
+                                              Classes="repeattrack"
+                                              Focusable="False"/>
                             </Track.IncreaseButton>
                             <Thumb Name="thumb">
                                 <Thumb.Template>
@@ -38,7 +41,8 @@
                         <RepeatButton Name="PART_LineDownButton"
                                       Classes="repeat"
                                       Grid.Row="2"
-                                      Grid.Column="2">
+                                      Grid.Column="2"
+                                      Focusable="False">
                             <Path Data="M 0,2.5 C0,2.5 0,0.5 0,0.5 0,0.5 3.5,4 3.5,4 3.5,4 7,0.5 7,0.5 7,0.5 7,2.5 7,2.5 7,2.5 3.5,6 3.5,6 3.5,6 0,2.5 0,2.5 z"
                                   Stretch="Uniform"
                                   Fill="{DynamicResource ThemeForegroundLowBrush}" />
@@ -58,7 +62,8 @@
                         <RepeatButton Name="PART_LineUpButton"
                                       Classes="repeat"
                                       Grid.Row="0"
-                                      Grid.Column="0">
+                                      Grid.Column="0"
+                                      Focusable="False">
                             <Path Data="M 3.18,7 C3.18,7 5,7 5,7 5,7 1.81,3.5 1.81,3.5 1.81,3.5 5,0 5,0 5,0 3.18,0 3.18,0 3.18,0 0,3.5 0,3.5 0,3.5 3.18,7 3.18,7 z"
                                   Stretch="Uniform"
                                   Fill="{DynamicResource ThemeForegroundLowBrush}" />
@@ -72,11 +77,13 @@
                                Orientation="{TemplateBinding Orientation}">
                             <Track.DecreaseButton>
                                 <RepeatButton Name="PART_PageUpButton"
-                                              Classes="repeattrack" />
+                                              Classes="repeattrack"
+                                              Focusable="False"/>
                             </Track.DecreaseButton>
                             <Track.IncreaseButton>
                                 <RepeatButton Name="PART_PageDownButton"
-                                              Classes="repeattrack" />
+                                              Classes="repeattrack"
+                                              Focusable="False"/>
                             </Track.IncreaseButton>
                             <Thumb Name="thumb">
                                 <Thumb.Template>
@@ -89,7 +96,8 @@
                         <RepeatButton Name="PART_LineDownButton"
                                       Classes="repeat"
                                       Grid.Row="2"
-                                      Grid.Column="2">
+                                      Grid.Column="2"
+                                      Focusable="False">
                             <Path Data="M 1.81,7 C1.81,7 0,7 0,7 0,7 3.18,3.5 3.18,3.5 3.18,3.5 0,0 0,0 0,0 1.81,0 1.81,0 1.81,0 5,3.5 5,3.5 5,3.5 1.81,7 1.81,7 z"
                                   Stretch="Uniform"
                                   Fill="{DynamicResource ThemeForegroundLowBrush}" />

+ 4 - 2
src/Avalonia.Themes.Default/ScrollViewer.xaml

@@ -19,14 +19,16 @@
                    Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}"
                    ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}"
                    Visibility="{TemplateBinding HorizontalScrollBarVisibility}"
-                   Grid.Row="1"/>
+                   Grid.Row="1"
+                   Focusable="False"/>
         <ScrollBar Name="verticalScrollBar"
                    Orientation="Vertical"
                    Maximum="{TemplateBinding VerticalScrollBarMaximum}"
                    Value="{TemplateBinding VerticalScrollBarValue, Mode=TwoWay}"
                    ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}"
                    Visibility="{TemplateBinding VerticalScrollBarVisibility}"
-                   Grid.Column="1"/>
+                   Grid.Column="1"
+                   Focusable="False"/>
       </Grid>
     </ControlTemplate>
   </Setter>

+ 15 - 0
src/Avalonia.Themes.Default/UserControl.xaml

@@ -0,0 +1,15 @@
+<Style xmlns="https://github.com/avaloniaui" Selector="UserControl">
+  <Setter Property="Template">
+    <ControlTemplate>
+      <ContentPresenter Name="PART_ContentPresenter"
+                        Background="{TemplateBinding Background}"
+                        BorderBrush="{TemplateBinding BorderBrush}"
+                        BorderThickness="{TemplateBinding BorderThickness}"
+                        ContentTemplate="{TemplateBinding ContentTemplate}"
+                        Content="{TemplateBinding Content}"
+                        Padding="{TemplateBinding Padding}"
+                        VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
+                        HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"/>
+    </ControlTemplate>
+  </Setter>
+</Style>

+ 2 - 1
src/Avalonia.Themes.Default/Window.xaml

@@ -1,5 +1,6 @@
 <Style xmlns="https://github.com/avaloniaui" Selector="Window">
   <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
+  <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
   <Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
   <Setter Property="Template">
     <ControlTemplate>
@@ -15,4 +16,4 @@
       </Border>
     </ControlTemplate>
   </Setter>
-</Style>
+</Style>

+ 15 - 1
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@@ -125,6 +125,20 @@ namespace Avalonia.Rendering
                 if (m.HasValue)
                 {
                     var bounds = new Rect(visual.Bounds.Size).TransformToAABB(m.Value);
+
+                    //use transformedbounds as previous render state of the visual bounds
+                    //so we can invalidate old and new bounds of a control in case it moved/shrinked
+                    if (visual.TransformedBounds.HasValue)
+                    {
+                        var trb = visual.TransformedBounds.Value;
+                        var trBounds = trb.Bounds.TransformToAABB(trb.Transform);
+
+                        if (trBounds != bounds)
+                        {
+                            _renderRoot?.Invalidate(trBounds);
+                        }
+                    }
+
                     _renderRoot?.Invalidate(bounds);
                 }
             }
@@ -191,7 +205,7 @@ namespace Avalonia.Rendering
             }
         }
 
-        static IEnumerable<IVisual> HitTest(
+        private static IEnumerable<IVisual> HitTest(
            IVisual visual,
            Point p,
            Func<IVisual, bool> filter)

+ 19 - 4
src/Avalonia.Visuals/Visual.cs

@@ -304,7 +304,7 @@ namespace Avalonia
             {
                 var thisOffset = GetOffsetFrom(common, this);
                 var thatOffset = GetOffsetFrom(common, visual);
-                return Matrix.CreateTranslation(-thatOffset) * Matrix.CreateTranslation(thisOffset);
+                return -thatOffset * thisOffset;
             }
 
             return null;
@@ -454,13 +454,28 @@ namespace Avalonia
         /// <param name="ancestor">The ancestor visual.</param>
         /// <param name="visual">The visual.</param>
         /// <returns>The visual offset.</returns>
-        private static Vector GetOffsetFrom(IVisual ancestor, IVisual visual)
+        private static Matrix GetOffsetFrom(IVisual ancestor, IVisual visual)
         {
-            var result = new Vector();
+            var result = Matrix.Identity;
 
             while (visual != ancestor)
             {
-                result = new Vector(result.X + visual.Bounds.X, result.Y + visual.Bounds.Y);
+                if (visual.RenderTransform?.Value != null)
+                {
+                    var origin = visual.RenderTransformOrigin.ToPixels(visual.Bounds.Size);
+                    var offset = Matrix.CreateTranslation(origin);
+                    var renderTransform = (-offset) * visual.RenderTransform.Value * (offset);
+
+                    result *= renderTransform;
+                }
+
+                var topLeft = visual.Bounds.TopLeft;
+
+                if (topLeft != default)
+                {
+                    result *= Matrix.CreateTranslation(topLeft);
+                }
+
                 visual = visual.VisualParent;
 
                 if (visual == null)

+ 1 - 1
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -121,7 +121,7 @@ namespace Avalonia.Skia
 
                 int offset = 0;
 
-                if (point.X >= (rects[line.Start].X + line.Width) / 2 && line.Length > 0)
+                if (point.X >= (rects[line.Start].X + line.Width) && line.Length > 0)
                 {
                     offset = line.TextLength > line.Length ?
                                     line.Length : (line.Length - 1);

+ 23 - 6
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListExtenionsTests.cs

@@ -1,5 +1,4 @@
-using System;
-using System.Linq;
+using System.Linq;
 using Avalonia.Collections;
 using Xunit;
 
@@ -82,13 +81,31 @@ namespace Avalonia.Base.UnitTests.Collections
             Assert.Equal(source, result);
         }
 
-        [Fact]
-        public void CreateDerivedList_Handles_MoveRange()
+        [Theory]
+        [InlineData(0, 2, 3)]
+        [InlineData(0, 2, 4)]
+        [InlineData(0, 2, 5)]
+        [InlineData(0, 4, 4)]
+        [InlineData(1, 2, 0)]
+        [InlineData(1, 2, 4)]
+        [InlineData(1, 2, 5)]
+        [InlineData(1, 4, 0)]
+        [InlineData(2, 2, 0)]
+        [InlineData(2, 2, 1)]
+        [InlineData(2, 2, 3)]
+        [InlineData(2, 2, 4)]
+        [InlineData(2, 2, 5)]
+        [InlineData(4, 2, 0)]
+        [InlineData(4, 2, 1)]
+        [InlineData(4, 2, 3)]
+        [InlineData(5, 1, 0)]
+        [InlineData(5, 1, 3)]
+        public void CreateDerivedList_Handles_MoveRange(int oldIndex, int count, int newIndex)
         {
-            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3 });
+            var source = new AvaloniaList<int>(new[] { 0, 1, 2, 3, 4, 5 });
             var target = source.CreateDerivedList(x => new Wrapper(x));
 
-            source.MoveRange(1, 2, 0);
+            source.MoveRange(oldIndex, count, newIndex);
 
             var result = target.Select(x => x.Value).ToList();
 

+ 206 - 2
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@@ -1,7 +1,12 @@
 using System;
 using System.Windows.Input;
 using Avalonia.Data;
-using Avalonia.Markup.Data;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.VisualTree;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -92,6 +97,205 @@ namespace Avalonia.Controls.UnitTests
             Assert.False(target.IsEnabled);
         }
 
+        [Fact]
+        public void Button_Raises_Click()
+        {
+            var mouse = Mock.Of<IMouseDevice>();
+            var renderer = Mock.Of<IRenderer>();
+            IInputElement captured = null;
+            Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(50, 50));
+            Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
+            Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+            Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
+                .Returns<Point, IVisual, Func<IVisual, bool>>((p, r, f) =>
+                    r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]);
+
+            var target = new TestButton()
+            {
+                Bounds = new Rect(0, 0, 100, 100),
+                Renderer = renderer
+            };
+
+            bool clicked = false;
+
+            target.Click += (s, e) => clicked = true;
+
+            RaisePointerEnter(target, mouse);
+            RaisePointerMove(target, mouse);
+            RaisePointerPressed(target, mouse, 1, MouseButton.Left);
+
+            Assert.Equal(captured, target);
+
+            RaisePointerReleased(target, mouse, MouseButton.Left);
+
+            Assert.Equal(captured, null);
+
+            Assert.True(clicked);
+        }
+
+        [Fact]
+        public void Button_Does_Not_Raise_Click_When_PointerReleased_Outside()
+        {
+            var mouse = Mock.Of<IMouseDevice>();
+            var renderer = Mock.Of<IRenderer>();
+            IInputElement captured = null;
+            Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(200, 50));
+            Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
+            Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+            Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
+                .Returns<Point, IVisual, Func<IVisual, bool>>((p, r, f) =>
+                    r.Bounds.Contains(p) ? new IVisual[] { r } : new IVisual[0]);
+
+            var target = new TestButton()
+            {
+                Bounds = new Rect(0, 0, 100, 100),
+                Renderer = renderer
+            };
+
+            bool clicked = false;
+
+            target.Click += (s, e) => clicked = true;
+
+            RaisePointerEnter(target, mouse);
+            RaisePointerMove(target, mouse);
+            RaisePointerPressed(target, mouse, 1, MouseButton.Left);
+            RaisePointerLeave(target, mouse);
+
+            Assert.Equal(captured, target);
+
+            RaisePointerReleased(target, mouse, MouseButton.Left);
+
+            Assert.Equal(captured, null);
+
+            Assert.False(clicked);
+        }
+
+        [Fact]
+        public void Button_With_RenderTransform_Raises_Click()
+        {
+            var mouse = Mock.Of<IMouseDevice>();
+            var renderer = Mock.Of<IRenderer>();
+            IInputElement captured = null;
+            Mock.Get(mouse).Setup(m => m.GetPosition(It.IsAny<IVisual>())).Returns(new Point(150, 50));
+            Mock.Get(mouse).Setup(m => m.Capture(It.IsAny<IInputElement>())).Callback<IInputElement>(v => captured = v);
+            Mock.Get(mouse).Setup(m => m.Captured).Returns(() => captured);
+            Mock.Get(renderer).Setup(r => r.HitTest(It.IsAny<Point>(), It.IsAny<IVisual>(), It.IsAny<Func<IVisual, bool>>()))
+                .Returns<Point, IVisual, Func<IVisual, bool>>((p, r, f) =>
+                    r.Bounds.Contains(p.Transform(r.RenderTransform.Value.Invert())) ?
+                    new IVisual[] { r } : new IVisual[0]);
+
+            var target = new TestButton()
+            {
+                Bounds = new Rect(0, 0, 100, 100),
+                RenderTransform = new TranslateTransform { X = 100, Y = 0 },
+                Renderer = renderer
+            };
+
+            //actual bounds of button should  be 100,0,100,100 x -> translated 100 pixels
+            //so mouse with x=150 coordinates should trigger click
+            //button shouldn't count on bounds to calculate pointer is in the over or not, but
+            //on avalonia event system, as renderer hit test will properly calculate whether to send
+            //mouse over events to button based on rendered bounds
+            //note: button also may have not rectangular shape and only renderer hit testing is reliable
+
+            bool clicked = false;
+
+            target.Click += (s, e) => clicked = true;
+
+            RaisePointerEnter(target, mouse);
+            RaisePointerMove(target, mouse);
+            RaisePointerPressed(target, mouse, 1, MouseButton.Left);
+
+            Assert.Equal(captured, target);
+
+            RaisePointerReleased(target, mouse, MouseButton.Left);
+
+            Assert.Equal(captured, null);
+
+            Assert.True(clicked);
+        }
+
+        private class TestButton : Button, IRenderRoot
+        {
+            public TestButton()
+            {
+                IsVisible = true;
+            }
+
+            public new Rect Bounds
+            {
+                get => base.Bounds;
+                set => base.Bounds = value;
+            }
+
+            public Size ClientSize => throw new NotImplementedException();
+
+            public IRenderer Renderer { get; set; }
+
+            public double RenderScaling => throw new NotImplementedException();
+
+            public IRenderTarget CreateRenderTarget() => throw new NotImplementedException();
+
+            public void Invalidate(Rect rect) => throw new NotImplementedException();
+
+            public Point PointToClient(Point point) => throw new NotImplementedException();
+
+            public Point PointToScreen(Point point) => throw new NotImplementedException();
+        }
+
+        private void RaisePointerPressed(Button button, IMouseDevice device, int clickCount, MouseButton mouseButton)
+        {
+            button.RaiseEvent(new PointerPressedEventArgs
+            {
+                RoutedEvent = InputElement.PointerPressedEvent,
+                Source = button,
+                MouseButton = mouseButton,
+                ClickCount = clickCount,
+                Device = device,
+            });
+        }
+
+        private void RaisePointerReleased(Button button, IMouseDevice device, MouseButton mouseButton)
+        {
+            button.RaiseEvent(new PointerReleasedEventArgs
+            {
+                RoutedEvent = InputElement.PointerReleasedEvent,
+                Source = button,
+                MouseButton = mouseButton,
+                Device = device,
+            });
+        }
+
+        private void RaisePointerEnter(Button button, IMouseDevice device)
+        {
+            button.RaiseEvent(new PointerEventArgs
+            {
+                RoutedEvent = InputElement.PointerEnterEvent,
+                Source = button,
+                Device = device,
+            });
+        }
+
+        private void RaisePointerLeave(Button button, IMouseDevice device)
+        {
+            button.RaiseEvent(new PointerEventArgs
+            {
+                RoutedEvent = InputElement.PointerLeaveEvent,
+                Source = button,
+                Device = device,
+            });
+        }
+
+        private void RaisePointerMove(Button button, IMouseDevice device)
+        {
+            button.RaiseEvent(new PointerEventArgs
+            {
+                RoutedEvent = InputElement.PointerMovedEvent,
+                Source = button,
+                Device = device,
+            });
+        }
+
         private class TestCommand : ICommand
         {
             private bool _enabled;
@@ -123,4 +327,4 @@ namespace Avalonia.Controls.UnitTests
             }
         }
     }
-}
+}

+ 2 - 2
tests/Avalonia.Controls.UnitTests/UserControlTests.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests
                 {
                     Styles =
                     {
-                        new Style(x => x.OfType<ContentControl>())
+                        new Style(x => x.OfType<UserControl>())
                         {
                             Setters = new[]
                             {
@@ -40,7 +40,7 @@ namespace Avalonia.Controls.UnitTests
 
         private FuncControlTemplate GetTemplate()
         {
-            return new FuncControlTemplate<ContentControl>(parent =>
+            return new FuncControlTemplate<UserControl>(parent =>
             {
                 return new Border
                 {

+ 98 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/ImmediateRendererTests.cs

@@ -0,0 +1,98 @@
+using System.Collections.Generic;
+using Avalonia.Collections;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering;
+using Avalonia.VisualTree;
+using Moq;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Rendering
+{
+    public class ImmediateRendererTests
+    {
+        [Fact]
+        public void AddDirty_Call_RenderRoot_Invalidate()
+        {
+            var visual = new Mock<Visual>();
+            var child = new Mock<Visual>() { CallBase = true };
+            var renderRoot = visual.As<IRenderRoot>();
+
+            visual.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(0, 0, 400, 400));
+
+            child.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(10, 10, 100, 100));
+            child.As<IVisual>().Setup(v => v.VisualParent).Returns(visual.Object);
+
+            var target = new ImmediateRenderer(visual.Object);
+
+            target.AddDirty(child.Object);
+
+            renderRoot.Verify(v => v.Invalidate(new Rect(10, 10, 100, 100)));
+        }
+
+        [Fact]
+        public void AddDirty_With_RenderTransform_Call_RenderRoot_Invalidate()
+        {
+            var visual = new Mock<Visual>();
+            var child = new Mock<Visual>() { CallBase = true };
+            var renderRoot = visual.As<IRenderRoot>();
+
+            visual.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(0, 0, 400, 400));
+
+            child.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(100, 100, 100, 100));
+            child.As<IVisual>().Setup(v => v.VisualParent).Returns(visual.Object);
+            child.Object.RenderTransform = new ScaleTransform() { ScaleX = 2, ScaleY = 2 };
+
+            var target = new ImmediateRenderer(visual.Object);
+
+            target.AddDirty(child.Object);
+
+            renderRoot.Verify(v => v.Invalidate(new Rect(50, 50, 200, 200)));
+        }
+
+        [Fact]
+        public void AddDirty_For_Child_Moved_Should_Invalidate_Previous_Bounds()
+        {
+            var visual = new Mock<Visual>() { CallBase = true };
+            var child = new Mock<Visual>() { CallBase = true };
+            var renderRoot = visual.As<IRenderRoot>();
+            var renderTarget = visual.As<IRenderTarget>();
+
+            renderRoot.Setup(r => r.CreateRenderTarget()).Returns(renderTarget.Object);
+            renderTarget.Setup(r => r.CreateDrawingContext(It.IsAny<IVisualBrushRenderer>())).Returns(Mock.Of<IDrawingContextImpl>());
+
+            visual.As<IVisual>().Setup(v => v.Bounds).Returns(new Rect(0, 0, 400, 400));
+            visual.As<IVisual>().Setup(v => v.VisualChildren).Returns(new AvaloniaList<IVisual>() { child.As<IVisual>().Object });
+
+            Rect childBounds = new Rect(0, 0, 100, 100);
+            child.As<IVisual>().Setup(v => v.Bounds).Returns(() => childBounds);
+            child.As<IVisual>().Setup(v => v.VisualParent).Returns(visual.Object);
+            child.As<IVisual>().Setup(v => v.VisualChildren).Returns(new AvaloniaList<IVisual>());
+
+            var invalidationCalls = new List<Rect>();
+
+            renderRoot.Setup(v => v.Invalidate(It.IsAny<Rect>())).Callback<Rect>(v => invalidationCalls.Add(v));
+
+            var target = new ImmediateRenderer(visual.Object);
+
+            target.AddDirty(child.Object);
+
+            Assert.Equal(new Rect(0, 0, 100, 100), invalidationCalls[0]);
+
+            target.Paint(new Rect(0, 0, 100, 100));
+
+            //move child 100 pixels bottom/right
+            childBounds = new Rect(100, 100, 100, 100);
+
+            //renderer should invalidate old child bounds with new one
+            //as on old area there can be artifacts
+            target.AddDirty(child.Object);
+
+            //invalidate first old position
+            Assert.Equal(new Rect(0, 0, 100, 100), invalidationCalls[1]);
+
+            //then new position
+            Assert.Equal(new Rect(100, 100, 100, 100), invalidationCalls[2]);
+        }
+    }
+}

+ 44 - 0
tests/Avalonia.Visuals.UnitTests/VisualTests.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Controls;
+using Avalonia.Media;
 using Avalonia.Rendering;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
@@ -192,5 +193,48 @@ namespace Avalonia.Visuals.UnitTests
             Assert.Throws<InvalidOperationException>(() => root2.Child = child);
             Assert.Empty(root2.GetVisualChildren());
         }
+
+        [Fact]
+        public void TransformToVisual_Should_Work()
+        {
+            var child = new Decorator { Width = 100, Height = 100 };
+            var root = new TestRoot() { Child = child, Width = 400, Height = 400 };
+
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(new Point(), root.DesiredSize));
+
+            var tr = child.TransformToVisual(root);
+
+            Assert.NotNull(tr);
+
+            var point = root.Bounds.TopLeft * tr;
+
+            //child is centered (400 - 100)/2
+            Assert.Equal(new Point(150, 150), point);
+        }
+
+        [Fact]
+        public void TransformToVisual_With_RenderTransform_Should_Work()
+        {
+            var child = new Decorator
+            {
+                Width = 100,
+                Height = 100,
+                RenderTransform = new ScaleTransform() { ScaleX = 2, ScaleY = 2 }
+            };
+            var root = new TestRoot() { Child = child, Width = 400, Height = 400 };
+
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(new Point(), root.DesiredSize));
+
+            var tr = child.TransformToVisual(root);
+
+            Assert.NotNull(tr);
+
+            var point = root.Bounds.TopLeft * tr;
+
+            //child is centered (400 - 100*2 scale)/2
+            Assert.Equal(new Point(100, 100), point);
+        }
     }
 }