Kaynağa Gözat

Merge branch 'master' into fixes/LineBreakCharacterTests

Dariusz Komosiński 5 yıl önce
ebeveyn
işleme
e383bd46b2
39 değiştirilmiş dosya ile 649 ekleme ve 176 silme
  1. 5 0
      .ncrunch/BindingDemo.v3.ncrunchproject
  2. 5 0
      .ncrunch/RenderDemo.v3.ncrunchproject
  3. 5 0
      .ncrunch/VirtualizationDemo.v3.ncrunchproject
  4. 4 4
      azure-pipelines.yml
  5. 7 1
      native/Avalonia.Native/inc/avalonia-native.h
  6. 60 21
      native/Avalonia.Native/src/OSX/window.mm
  7. 9 2
      samples/ControlCatalog/MainView.xaml
  8. 14 0
      samples/ControlCatalog/MainView.xaml.cs
  9. 18 1
      samples/ControlCatalog/Pages/ImagePage.xaml
  10. 33 0
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  11. 5 1
      src/Avalonia.Base/AvaloniaObject.cs
  12. 47 41
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  13. 1 1
      src/Avalonia.Controls/Calendar/DatePicker.cs
  14. 2 1
      src/Avalonia.Controls/ComboBox.cs
  15. 1 1
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  16. 1 1
      src/Avalonia.Controls/Primitives/Popup.cs
  17. 45 15
      src/Avalonia.Controls/TextBlock.cs
  18. 37 4
      src/Avalonia.Controls/TextBox.cs
  19. 82 14
      src/Avalonia.Controls/Window.cs
  20. 1 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  21. 1 1
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  22. 1 1
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  23. 2 2
      src/Avalonia.Interactivity/IInteractive.cs
  24. 6 26
      src/Avalonia.Interactivity/Interactive.cs
  25. 24 1
      src/Avalonia.Interactivity/InteractiveExtensions.cs
  26. 2 2
      src/Avalonia.Native/WindowImpl.cs
  27. 6 3
      src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs
  28. 96 0
      src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs
  29. 18 9
      src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
  30. 11 5
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  31. 1 0
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  32. 8 2
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  33. 11 5
      src/Avalonia.X11/X11Window.cs
  34. 10 0
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  35. 12 8
      src/Windows/Avalonia.Win32/WindowImpl.cs
  36. 1 1
      src/iOS/Avalonia.iOS/EmbeddableImpl.cs
  37. 11 0
      tests/Avalonia.Benchmarks/NullGlyphRun.cs
  38. 3 1
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  39. 43 0
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

+ 5 - 0
.ncrunch/BindingDemo.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 5 - 0
.ncrunch/RenderDemo.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 5 - 0
.ncrunch/VirtualizationDemo.v3.ncrunchproject

@@ -0,0 +1,5 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
+  </Settings>
+</ProjectConfiguration>

+ 4 - 4
azure-pipelines.yml

@@ -35,16 +35,16 @@ jobs:
     vmImage: 'macOS-10.14'
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 3.1.x'
+    displayName: 'Use .NET Core SDK 3.1.101'
     inputs:
       packageType: sdk
-      version: 3.1.x
+      version: 3.1.101
 
   - task: UseDotNet@2
-    displayName: 'Use .NET Core Runtime 3.1.x'
+    displayName: 'Use .NET Core Runtime 3.1.1'
     inputs:
       packageType: runtime
-      version: 3.1.x
+      version: 3.1.1
 
   - task: CmdLine@2
     displayName: 'Install Mono 5.18'

+ 7 - 1
native/Avalonia.Native/inc/avalonia-native.h

@@ -25,6 +25,12 @@ struct IAvnGlSurfaceRenderingSession;
 struct IAvnAppMenu;
 struct IAvnAppMenuItem;
 
+enum SystemDecorations {
+    SystemDecorationsNone = 0,
+    SystemDecorationsBorderOnly = 1,
+    SystemDecorationsFull = 2,
+};
+
 struct AvnSize
 {
     double Width, Height;
@@ -236,7 +242,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
 {
     virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
     virtual HRESULT SetCanResize(bool value) = 0;
-    virtual HRESULT SetHasDecorations(bool value) = 0;
+    virtual HRESULT SetHasDecorations(SystemDecorations value) = 0;
     virtual HRESULT SetTitle (void* utf8Title) = 0;
     virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
     virtual HRESULT SetWindowState(AvnWindowState state) = 0;

+ 60 - 21
native/Avalonia.Native/src/OSX/window.mm

@@ -115,7 +115,6 @@ public:
             [NSApp activateIgnoringOtherApps:YES];
             
             [Window setTitle:_lastTitle];
-            [Window setTitleVisibility:NSWindowTitleVisible];
         
             return S_OK;
         }
@@ -411,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub
 {
 private:
     bool _canResize = true;
-    bool _hasDecorations = true;
+    SystemDecorations _hasDecorations = SystemDecorationsFull;
     CGRect _lastUndecoratedFrame;
     AvnWindowState _lastWindowState;
     
@@ -476,23 +475,26 @@ private:
     
     bool IsZoomed ()
     {
-        return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized();
+        return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized();
     }
     
     void DoZoom()
     {
-        if (_hasDecorations)
+        switch (_hasDecorations)
         {
-            [Window performZoom:Window];
-        }
-        else
-        {
-            if (!UndecoratedIsMaximized())
-            {
-                _lastUndecoratedFrame = [Window frame];
-            }
-            
-            [Window zoom:Window];
+            case SystemDecorationsNone:
+                if (!UndecoratedIsMaximized())
+                {
+                    _lastUndecoratedFrame = [Window frame];
+                }
+                
+                [Window zoom:Window];
+                break;
+
+            case SystemDecorationsBorderOnly:
+            case SystemDecorationsFull:
+                [Window performZoom:Window];
+                break;
         }
     }
     
@@ -506,13 +508,35 @@ private:
         }
     }
     
-    virtual HRESULT SetHasDecorations(bool value) override
+    virtual HRESULT SetHasDecorations(SystemDecorations value) override
     {
         @autoreleasepool
         {
             _hasDecorations = value;
             UpdateStyle();
-            
+
+            switch (_hasDecorations)
+            {
+                case SystemDecorationsNone:
+                    [Window setHasShadow:NO];
+                    [Window setTitleVisibility:NSWindowTitleHidden];
+                    [Window setTitlebarAppearsTransparent:YES];
+                    break;
+
+                case SystemDecorationsBorderOnly:
+                    [Window setHasShadow:YES];
+                    [Window setTitleVisibility:NSWindowTitleHidden];
+                    [Window setTitlebarAppearsTransparent:YES];
+                    break;
+
+                case SystemDecorationsFull:
+                    [Window setHasShadow:YES];
+                    [Window setTitleVisibility:NSWindowTitleVisible];
+                    [Window setTitlebarAppearsTransparent:NO];
+                    [Window setTitle:_lastTitle];
+                    break;
+            }
+
             return S_OK;
         }
     }
@@ -523,7 +547,6 @@ private:
         {
             _lastTitle = [NSString stringWithUTF8String:(const char*)utf8title];
             [Window setTitle:_lastTitle];
-            [Window setTitleVisibility:NSWindowTitleVisible];
             
             return S_OK;
         }
@@ -645,10 +668,26 @@ protected:
     virtual NSWindowStyleMask GetStyle() override
     {
         unsigned long s = NSWindowStyleMaskBorderless;
-        if(_hasDecorations)
-            s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
-        if(_canResize)
-            s = s | NSWindowStyleMaskResizable;
+
+        switch (_hasDecorations)
+        {
+            case SystemDecorationsNone:
+                break;
+
+            case SystemDecorationsBorderOnly:
+                s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
+                break;
+
+            case SystemDecorationsFull:
+                s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless;
+                if(_canResize)
+                {
+                    s = s | NSWindowStyleMaskResizable;
+                }
+
+                break;
+        }
+
         return s;
     }
 };

+ 9 - 2
samples/ControlCatalog/MainView.xaml

@@ -59,10 +59,17 @@
       <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
       <TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
       <TabControl.Tag>
-        <ComboBox x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
+        <StackPanel Width="115" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
+          <ComboBox x:Name="Decorations" SelectedIndex="0" Margin="0,0,0,8">
+            <ComboBoxItem>No Decorations</ComboBoxItem>
+            <ComboBoxItem>Border Only</ComboBoxItem>
+            <ComboBoxItem>Full Decorations</ComboBoxItem>
+          </ComboBox>
+          <ComboBox x:Name="Themes" SelectedIndex="0">
             <ComboBoxItem>Light</ComboBoxItem>
             <ComboBoxItem>Dark</ComboBoxItem>
-        </ComboBox>
+          </ComboBox>
+        </StackPanel>
       </TabControl.Tag>
     </TabControl>
   </Grid>

+ 14 - 0
samples/ControlCatalog/MainView.xaml.cs

@@ -56,6 +56,20 @@ namespace ControlCatalog
                 }
             };
             Styles.Add(light);
+
+            var decorations = this.Find<ComboBox>("Decorations");
+            decorations.SelectionChanged += (sender, e) =>
+            {
+                Window window = (Window)VisualRoot;
+                window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
+            };
+        }
+
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+            var decorations = this.Find<ComboBox>("Decorations");
+            decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations;
         }
     }
 }

+ 18 - 1
samples/ControlCatalog/Pages/ImagePage.xaml

@@ -7,7 +7,7 @@
       <TextBlock Classes="h2">Displays an image</TextBlock>
     </StackPanel>
 
-    <Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*" Margin="64">
+    <Grid ColumnDefinitions="*,*,*" RowDefinitions="Auto,*" Margin="64">
       
       <DockPanel Grid.Column="0" Grid.Row="1" Margin="16">
         <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Bitmap</TextBlock>
@@ -22,6 +22,23 @@
       </DockPanel>
 
       <DockPanel Grid.Column="1" Grid.Row="1" Margin="16">
+        <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Crop</TextBlock>
+        <ComboBox Name="bitmapCrop" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="BitmapCropChanged">
+          <ComboBoxItem>None</ComboBoxItem>
+          <ComboBoxItem>Center</ComboBoxItem>
+          <ComboBoxItem>TopLeft</ComboBoxItem>
+          <ComboBoxItem>TopRight</ComboBoxItem>
+          <ComboBoxItem>BottomLeft</ComboBoxItem>
+          <ComboBoxItem>BottomRight</ComboBoxItem>
+        </ComboBox>
+        <Image Name="croppedImage">
+          <Image.Source>
+            <CroppedBitmap  Source="/Assets/delicate-arch-896885_640.jpg" SourceRect="0 0 320 240"/>  
+          </Image.Source>
+        </Image>
+      </DockPanel>
+
+      <DockPanel Grid.Column="2" Grid.Row="1" Margin="16">
         <TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Drawing</TextBlock>
         <ComboBox Name="drawingStretch" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="DrawingStretchChanged">
           <ComboBoxItem>None</ComboBoxItem>

+ 33 - 0
samples/ControlCatalog/Pages/ImagePage.xaml.cs

@@ -1,6 +1,10 @@
+using System;
+using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
 
 namespace ControlCatalog.Pages
 {
@@ -8,12 +12,14 @@ namespace ControlCatalog.Pages
     {
         private readonly Image _bitmapImage;
         private readonly Image _drawingImage;
+        private readonly Image _croppedImage;
 
         public ImagePage()
         {
             InitializeComponent();
             _bitmapImage = this.FindControl<Image>("bitmapImage");
             _drawingImage = this.FindControl<Image>("drawingImage");
+            _croppedImage = this.FindControl<Image>("croppedImage");
         }
 
         private void InitializeComponent()
@@ -38,5 +44,32 @@ namespace ControlCatalog.Pages
                 _drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex;
             }
         }
+
+        public void BitmapCropChanged(object sender, SelectionChangedEventArgs e)
+        {
+            if (_croppedImage != null)
+            {
+                var comboxBox = (ComboBox)sender;
+                var croppedBitmap = _croppedImage.Source as CroppedBitmap;
+                croppedBitmap.SourceRect = GetCropRect(comboxBox.SelectedIndex);
+            }
+        }
+
+        private PixelRect GetCropRect(int index)
+        {
+            var bitmapWidth = 640;
+            var bitmapHeight = 426;
+            var cropSize = new PixelSize(320, 240);
+            return index switch
+            {
+                1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize),
+                2 => new PixelRect(new PixelPoint(0, 0), cropSize),
+                3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize),
+                4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize),
+                5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize),
+                _ => PixelRect.Empty
+            };
+            
+        }
     }
 }

+ 5 - 1
src/Avalonia.Base/AvaloniaObject.cs

@@ -80,8 +80,12 @@ namespace Avalonia
                     _inheritanceParent?.RemoveInheritanceChild(this);
                     _inheritanceParent = value;
 
-                    foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()))
+                    var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType());
+                    var propertiesCount = properties.Count;
+
+                    for (var i = 0; i < propertiesCount; i++)
                     {
+                        var property = properties[i];
                         if (valuestore?.IsSet(property) == true)
                         {
                             // If local value set there can be no change.

+ 47 - 41
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -3,9 +3,7 @@
 
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Runtime.CompilerServices;
-using Avalonia.Data;
 
 namespace Avalonia
 {
@@ -28,8 +26,6 @@ namespace Avalonia
             new Dictionary<Type, List<AvaloniaProperty>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _directCache =
             new Dictionary<Type, List<AvaloniaProperty>>();
-        private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
-            new Dictionary<Type, List<PropertyInitializationData>>();
         private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
             new Dictionary<Type, List<AvaloniaProperty>>();
 
@@ -49,7 +45,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegistered(Type type)
+        public IReadOnlyList<AvaloniaProperty> GetRegistered(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
@@ -83,7 +79,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegisteredAttached(Type type)
+        public IReadOnlyList<AvaloniaProperty> GetRegisteredAttached(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
@@ -114,7 +110,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegisteredDirect(Type type)
+        public IReadOnlyList<AvaloniaProperty> GetRegisteredDirect(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
@@ -145,7 +141,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="type">The type.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegisteredInherited(Type type)
+        public IReadOnlyList<AvaloniaProperty> GetRegisteredInherited(Type type)
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
@@ -157,16 +153,27 @@ namespace Avalonia
             result = new List<AvaloniaProperty>();
             var visited = new HashSet<AvaloniaProperty>();
 
-            foreach (var property in GetRegistered(type))
+            var registered = GetRegistered(type);
+            var registeredCount = registered.Count;
+
+            for (var i = 0; i < registeredCount; i++)
             {
+                var property = registered[i];
+
                 if (property.Inherits)
                 {
                     result.Add(property);
                     visited.Add(property);
                 }
             }
-            foreach (var property in GetRegisteredAttached(type))
+
+            var registeredAttached = GetRegisteredAttached(type);
+            var registeredAttachedCount = registeredAttached.Count;
+
+            for (var i = 0; i < registeredAttachedCount; i++)
             {
+                var property = registeredAttached[i];
+
                 if (property.Inherits)
                 {
                     if (!visited.Contains(property))
@@ -185,7 +192,7 @@ namespace Avalonia
         /// </summary>
         /// <param name="o">The object.</param>
         /// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
-        public IEnumerable<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
+        public IReadOnlyList<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
         {
             Contract.Requires<ArgumentNullException>(o != null);
 
@@ -229,8 +236,13 @@ namespace Avalonia
                 throw new InvalidOperationException("Attached properties not supported.");
             }
 
-            foreach (AvaloniaProperty x in GetRegistered(type))
+            var registered = GetRegistered(type);
+            var registeredCount = registered.Count;
+
+            for (var i = 0; i < registeredCount; i++)
             {
+                AvaloniaProperty x = registered[i];
+
                 if (x.Name == name)
                 {
                     return x;
@@ -276,8 +288,13 @@ namespace Avalonia
                 return property;
             }
 
-            foreach (var p in GetRegisteredDirect(o.GetType()))
+            var registeredDirect = GetRegisteredDirect(o.GetType());
+            var registeredDirectCount = registeredDirect.Count;
+
+            for (var i = 0; i < registeredDirectCount; i++)
             {
+                var p = registeredDirect[i];
+
                 if (p == property)
                 {
                     return (DirectPropertyBase<T>)p;
@@ -308,8 +325,23 @@ namespace Avalonia
             Contract.Requires<ArgumentNullException>(type != null);
             Contract.Requires<ArgumentNullException>(property != null);
 
-            return Instance.GetRegistered(type).Any(x => x == property) ||
-                Instance.GetRegisteredAttached(type).Any(x => x == property);
+            static bool ContainsProperty(IReadOnlyList<AvaloniaProperty> properties, AvaloniaProperty property)
+            {
+                var propertiesCount = properties.Count;
+
+                for (var i = 0; i < propertiesCount; i++)
+                {
+                    if (properties[i] == property)
+                    {
+                        return true;
+                    }
+                }
+
+                return false;
+            }
+
+            return ContainsProperty(Instance.GetRegistered(type), property) ||
+                   ContainsProperty(Instance.GetRegisteredAttached(type), property);
         }
 
         /// <summary>
@@ -374,7 +406,6 @@ namespace Avalonia
             }
             
             _registeredCache.Clear();
-            _initializedCache.Clear();
             _inheritedCache.Clear();
         }
 
@@ -411,32 +442,7 @@ namespace Avalonia
             }
             
             _attachedCache.Clear();
-            _initializedCache.Clear();
             _inheritedCache.Clear();
         }
-
-        private readonly struct PropertyInitializationData
-        {
-            public AvaloniaProperty Property { get; }
-            public object Value { get; }
-            public bool IsDirect { get; }
-            public IDirectPropertyAccessor DirectAccessor { get; }
-
-            public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor)
-            {
-                Property = property;
-                Value = null;
-                IsDirect = true;
-                DirectAccessor = directAccessor;
-            }
-
-            public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type)
-            {
-                Property = property;
-                Value = styledAccessor.GetDefaultValue(type);
-                IsDirect = false;
-                DirectAccessor = null;
-            }
-        }
     }
 }

+ 1 - 1
src/Avalonia.Controls/Calendar/DatePicker.cs

@@ -476,7 +476,7 @@ namespace Avalonia.Controls
             {
                 _dropDownButton.Click += DropDownButton_Click;
                 _buttonPointerPressedSubscription =
-                    _dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
+                    _dropDownButton.AddDisposableHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
             }
 
             if (_textBox != null)

+ 2 - 1
src/Avalonia.Controls/ComboBox.cs

@@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.VisualTree;
@@ -265,7 +266,7 @@ namespace Avalonia.Controls
             var toplevel = this.GetVisualRoot() as TopLevel;
             if (toplevel != null)
             {
-                _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) =>
+                _subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
                 {
                     //eat wheel scroll event outside dropdown popup while it's open
                     if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)

+ 1 - 1
src/Avalonia.Controls/Platform/IWindowImpl.cs

@@ -36,7 +36,7 @@ namespace Avalonia.Platform
         /// <summary>
         /// Enables or disables system window decorations (title bar, buttons, etc)
         /// </summary>
-        void SetSystemDecorations(bool enabled);
+        void SetSystemDecorations(SystemDecorations enabled);
 
         /// <summary>
         /// Sets the icon of this window.

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

@@ -279,7 +279,7 @@ namespace Avalonia.Controls.Primitives
                 }
             }
 
-            DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
+            DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
 
             DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
 

+ 45 - 15
src/Avalonia.Controls/TextBlock.cs

@@ -20,6 +20,12 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<IBrush> BackgroundProperty =
             Border.BackgroundProperty.AddOwner<TextBlock>();
 
+        /// <summary>
+        /// Defines the <see cref="Padding"/> property.
+        /// </summary>
+        public static readonly StyledProperty<Thickness> PaddingProperty =
+            Decorator.PaddingProperty.AddOwner<TextBlock>();
+
         // TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner
         // them into TextBlock.
 
@@ -29,7 +35,7 @@ namespace Avalonia.Controls
         public static readonly AttachedProperty<FontFamily> FontFamilyProperty =
             AvaloniaProperty.RegisterAttached<TextBlock, Control, FontFamily>(
                 nameof(FontFamily),
-                defaultValue:  FontFamily.Default,
+                defaultValue: FontFamily.Default,
                 inherits: true);
 
         /// <summary>
@@ -110,20 +116,31 @@ namespace Avalonia.Controls
         static TextBlock()
         {
             ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
+
             AffectsRender<TextBlock>(
-                BackgroundProperty,
-                ForegroundProperty,
-                FontWeightProperty,
-                FontSizeProperty,
-                FontStyleProperty);
+                BackgroundProperty, ForegroundProperty, FontSizeProperty, 
+                FontWeightProperty, FontStyleProperty, TextWrappingProperty, 
+                TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty, 
+                TextDecorationsProperty, TextProperty, PaddingProperty);
+
+            AffectsMeasure<TextBlock>(
+                FontSizeProperty, FontWeightProperty, FontStyleProperty, 
+                FontFamilyProperty, TextTrimmingProperty, TextProperty,
+                PaddingProperty);
 
             Observable.Merge(
                 TextProperty.Changed,
+                ForegroundProperty.Changed,
                 TextAlignmentProperty.Changed,
+                TextWrappingProperty.Changed,
+                TextTrimmingProperty.Changed,
                 FontSizeProperty.Changed,
                 FontStyleProperty.Changed,
-                FontWeightProperty.Changed
-            ).AddClassHandler<TextBlock>((x, _) => x.OnTextPropertiesChanged());
+                FontWeightProperty.Changed,
+                FontFamilyProperty.Changed,
+                TextDecorationsProperty.Changed,
+                PaddingProperty.Changed
+            ).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout());
         }
 
         /// <summary>
@@ -145,6 +162,15 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Gets or sets the padding to place around the <see cref="Text"/>.
+        /// </summary>
+        public Thickness Padding
+        {
+            get { return GetValue(PaddingProperty); }
+            set { SetValue(PaddingProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets a brush used to paint the control's background.
         /// </summary>
@@ -363,7 +389,9 @@ namespace Avalonia.Controls
                 context.FillRectangle(background, new Rect(Bounds.Size));
             }
 
-            TextLayout?.Draw(context.PlatformImpl, new Point());
+            var padding = Padding;
+
+            TextLayout?.Draw(context.PlatformImpl, new Point(padding.Left, padding.Top));
         }
 
         /// <summary>
@@ -412,6 +440,10 @@ namespace Avalonia.Controls
                 return new Size();
             }
 
+            var padding = Padding;
+
+            availableSize = availableSize.Deflate(padding);
+
             if (_constraint != availableSize)
             {
                 InvalidateTextLayout();
@@ -419,19 +451,17 @@ namespace Avalonia.Controls
 
             _constraint = availableSize;
 
-            return TextLayout?.Bounds.Size ?? Size.Empty;
+            var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty;
+
+            return measuredSize.Inflate(padding);
         }
 
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnAttachedToLogicalTree(e);
-            InvalidateTextLayout();
-            InvalidateMeasure();
-        }
 
-        private void OnTextPropertiesChanged()
-        {
             InvalidateTextLayout();
+
             InvalidateMeasure();
         }
     }

+ 37 - 4
src/Avalonia.Controls/TextBox.cs

@@ -275,6 +275,22 @@ namespace Avalonia.Controls
             }
         }
 
+        public string SelectedText
+        {
+            get { return GetSelection(); }
+            set
+            {
+                if (value == null)
+                {
+                    return;
+                }
+
+                _undoRedoHelper.Snapshot();
+                HandleTextInput(value);
+                _undoRedoHelper.Snapshot();
+            }
+        }
+
         /// <summary>
         /// Gets or sets the horizontal alignment of the content within the control.
         /// </summary>
@@ -683,12 +699,12 @@ namespace Avalonia.Controls
 
         protected override void OnPointerPressed(PointerPressedEventArgs e)
         {
-            var point = e.GetPosition(_presenter);
-            var index = CaretIndex = _presenter.GetCaretIndex(point);
             var text = Text;
 
-            if (text != null && e.MouseButton == MouseButton.Left)
+            if (text != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
             {
+                var point = e.GetPosition(_presenter);
+                var index = CaretIndex = _presenter.GetCaretIndex(point);
                 switch (e.ClickCount)
                 {
                     case 1:
@@ -714,7 +730,8 @@ namespace Avalonia.Controls
 
         protected override void OnPointerMoved(PointerEventArgs e)
         {
-            if (_presenter != null && e.Pointer.Captured == _presenter)
+            // selection should not change during pointer move if the user right clicks
+            if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
             {
                 var point = e.GetPosition(_presenter);
 
@@ -727,6 +744,22 @@ namespace Avalonia.Controls
         {
             if (_presenter != null && e.Pointer.Captured == _presenter)
             {
+                if (e.InitialPressMouseButton == MouseButton.Right)
+                {
+                    var point = e.GetPosition(_presenter);
+                    var caretIndex = _presenter.GetCaretIndex(point);
+
+                    // see if mouse clicked inside current selection
+                    // if it did not, we change the selection to where the user clicked
+                    var firstSelection = Math.Min(SelectionStart, SelectionEnd);
+                    var lastSelection = Math.Max(SelectionStart, SelectionEnd);
+                    var didClickInSelection = SelectionStart != SelectionEnd && 
+                        caretIndex >= firstSelection && caretIndex <= lastSelection;
+                    if (!didClickInSelection)
+                    {
+                        CaretIndex = SelectionEnd = SelectionStart = caretIndex;
+                    }
+                }
                 e.Pointer.Capture(null);
             }
         }

+ 82 - 14
src/Avalonia.Controls/Window.cs

@@ -2,19 +2,19 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.ComponentModel;
+using System.Linq;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
 using Avalonia.Controls.Platform;
+using Avalonia.Data;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Styling;
-using System.Collections.Generic;
-using System.Linq;
 using JetBrains.Annotations;
-using System.ComponentModel;
-using Avalonia.Interactivity;
 
 namespace Avalonia.Controls
 {
@@ -45,6 +45,27 @@ namespace Avalonia.Controls
         WidthAndHeight = 3,
     }
 
+    /// <summary>
+    /// Determines system decorations (title bar, border, etc) for a <see cref="Window"/>
+    /// </summary>
+    public enum SystemDecorations
+    {
+        /// <summary>
+        /// No decorations
+        /// </summary>
+        None = 0,
+
+        /// <summary>
+        /// Window border without titlebar
+        /// </summary>
+        BorderOnly = 1,
+
+        /// <summary>
+        /// Fully decorated (default)
+        /// </summary>
+        Full = 2
+    }
+
     /// <summary>
     /// A top-level window.
     /// </summary>
@@ -59,8 +80,18 @@ namespace Avalonia.Controls
         /// <summary>
         /// Enables or disables system window decorations (title bar, buttons, etc)
         /// </summary>
-        public static readonly StyledProperty<bool> HasSystemDecorationsProperty =
-            AvaloniaProperty.Register<Window, bool>(nameof(HasSystemDecorations), true);
+        [Obsolete("Use SystemDecorationsProperty instead")]
+        public static readonly DirectProperty<Window, bool> HasSystemDecorationsProperty =
+            AvaloniaProperty.RegisterDirect<Window, bool>(
+                nameof(HasSystemDecorations),
+                o => o.HasSystemDecorations,
+                (o, v) => o.HasSystemDecorations = v);
+
+        /// <summary>
+        /// Defines the <see cref="SystemDecorations"/> property.
+        /// </summary>
+        public static readonly StyledProperty<SystemDecorations> SystemDecorationsProperty =
+            AvaloniaProperty.Register<Window, SystemDecorations>(nameof(SystemDecorations), SystemDecorations.Full);
 
         /// <summary>
         /// Enables or disables the taskbar icon
@@ -124,9 +155,6 @@ namespace Avalonia.Controls
         {
             BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White);
             TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue));
-            HasSystemDecorationsProperty.Changed.AddClassHandler<Window>(
-                (s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue));
-
             ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
 
             IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl));
@@ -135,12 +163,11 @@ namespace Avalonia.Controls
 
             WindowStateProperty.Changed.AddClassHandler<Window>(
                 (w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; });
-            
+
             MinWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
             MinHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
             MaxWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
             MaxHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
-
         }
 
         /// <summary>
@@ -191,11 +218,30 @@ namespace Avalonia.Controls
         /// <summary>
         /// Enables or disables system window decorations (title bar, buttons, etc)
         /// </summary>
-        /// 
+        [Obsolete("Use SystemDecorations instead")]
         public bool HasSystemDecorations
         {
-            get { return GetValue(HasSystemDecorationsProperty); }
-            set { SetValue(HasSystemDecorationsProperty, value); }
+            get => SystemDecorations == SystemDecorations.Full;
+            set
+            {
+                var oldValue = HasSystemDecorations;
+
+                if (oldValue != value)
+                {
+                    SystemDecorations = value ? SystemDecorations.Full : SystemDecorations.None;
+                    RaisePropertyChanged(HasSystemDecorationsProperty, oldValue, value);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Sets the system decorations (title bar, border, etc)
+        /// </summary>
+        /// 
+        public SystemDecorations SystemDecorations
+        {
+            get { return GetValue(SystemDecorationsProperty); }
+            set { SetValue(SystemDecorationsProperty, value); }
         }
 
         /// <summary>
@@ -584,5 +630,27 @@ namespace Avalonia.Controls
         /// <see cref="Closing"/> event needs to be raised.
         /// </remarks>
         protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e);
+
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            if (property == SystemDecorationsProperty)
+            {
+                var typedNewValue = newValue.GetValueOrDefault<SystemDecorations>();
+
+                PlatformImpl?.SetSystemDecorations(typedNewValue);
+
+                var o = oldValue.GetValueOrDefault<SystemDecorations>() == SystemDecorations.Full;
+                var n = typedNewValue == SystemDecorations.Full;
+
+                if (o != n)
+                {
+                    RaisePropertyChanged(HasSystemDecorationsProperty, o, n);
+                }
+            }
+        }
     }
 }

+ 1 - 1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@@ -96,7 +96,7 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(SystemDecorations enabled)
         {
         }
 

+ 1 - 1
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -110,7 +110,7 @@ namespace Avalonia.DesignerSupport.Remote
         {
         }
 
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(SystemDecorations enabled)
         {
         }
 

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Diagnostics
                 }
             }
 
-            return root.AddHandler(
+            return root.AddDisposableHandler(
                 InputElement.KeyDownEvent,
                 PreviewKeyDown,
                 RoutingStrategies.Tunnel);

+ 2 - 2
src/Avalonia.Interactivity/IInteractive.cs

@@ -23,7 +23,7 @@ namespace Avalonia.Interactivity
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
         /// <returns>A disposable that terminates the event subscription.</returns>
-        IDisposable AddHandler(
+        void AddHandler(
             RoutedEvent routedEvent,
             Delegate handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
@@ -38,7 +38,7 @@ namespace Avalonia.Interactivity
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
         /// <returns>A disposable that terminates the event subscription.</returns>
-        IDisposable AddHandler<TEventArgs>(
+        void AddHandler<TEventArgs>(
             RoutedEvent<TEventArgs> routedEvent,
             EventHandler<TEventArgs> handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,

+ 6 - 26
src/Avalonia.Interactivity/Interactive.cs

@@ -27,8 +27,7 @@ namespace Avalonia.Interactivity
         /// <param name="handler">The handler.</param>
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
-        /// <returns>A disposable that terminates the event subscription.</returns>
-        public IDisposable AddHandler(
+        public void AddHandler(
             RoutedEvent routedEvent,
             Delegate handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
@@ -38,7 +37,8 @@ namespace Avalonia.Interactivity
             handler = handler ?? throw new ArgumentNullException(nameof(handler));
 
             var subscription = new EventSubscription(handler, routes, handledEventsToo);
-            return AddEventSubscription(routedEvent, subscription);
+
+            AddEventSubscription(routedEvent, subscription);
         }
 
         /// <summary>
@@ -49,8 +49,7 @@ namespace Avalonia.Interactivity
         /// <param name="handler">The handler.</param>
         /// <param name="routes">The routing strategies to listen to.</param>
         /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
-        /// <returns>A disposable that terminates the event subscription.</returns>
-        public IDisposable AddHandler<TEventArgs>(
+        public void AddHandler<TEventArgs>(
             RoutedEvent<TEventArgs> routedEvent,
             EventHandler<TEventArgs> handler,
             RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
@@ -69,7 +68,7 @@ namespace Avalonia.Interactivity
 
             var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args));
 
-            return AddEventSubscription(routedEvent, subscription);
+            AddEventSubscription(routedEvent, subscription);
         }
 
         /// <summary>
@@ -188,7 +187,7 @@ namespace Avalonia.Interactivity
             return result;
         }
 
-        private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
+        private void AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
         {
             _eventHandlers ??= new Dictionary<RoutedEvent, List<EventSubscription>>();
 
@@ -199,8 +198,6 @@ namespace Avalonia.Interactivity
             }
 
             subscriptions.Add(subscription);
-
-            return new UnsubscribeDisposable(subscriptions, subscription);
         }
 
         private readonly struct EventSubscription
@@ -225,22 +222,5 @@ namespace Avalonia.Interactivity
 
             public bool HandledEventsToo { get; }
         }
-
-        private sealed class UnsubscribeDisposable : IDisposable
-        {
-            private readonly List<EventSubscription> _subscriptions;
-            private readonly EventSubscription _subscription;
-
-            public UnsubscribeDisposable(List<EventSubscription> subscriptions, EventSubscription subscription)
-            {
-                _subscriptions = subscriptions;
-                _subscription = subscription;
-            }
-
-            public void Dispose()
-            {
-                _subscriptions.Remove(_subscription);
-            }
-        }
     }
 }

+ 24 - 1
src/Avalonia.Interactivity/InteractiveExtensions.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Reactive.Disposables;
 using System.Reactive.Linq;
 
 namespace Avalonia.Interactivity
@@ -11,6 +12,28 @@ namespace Avalonia.Interactivity
     /// </summary>
     public static class InteractiveExtensions
     {
+        /// <summary>
+        /// Adds a handler for the specified routed event and returns a disposable that can terminate the event subscription.
+        /// </summary>
+        /// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
+        /// <param name="o">Target for adding given event handler.</param>
+        /// <param name="routedEvent">The routed event.</param>
+        /// <param name="handler">The handler.</param>
+        /// <param name="routes">The routing strategies to listen to.</param>
+        /// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
+        /// <returns>A disposable that terminates the event subscription.</returns>
+        public static IDisposable AddDisposableHandler<TEventArgs>(this IInteractive o, RoutedEvent<TEventArgs> routedEvent,
+            EventHandler<TEventArgs> handler,
+            RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
+            bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
+        {
+            o.AddHandler(routedEvent, handler, routes, handledEventsToo);
+
+            return Disposable.Create(
+                (instance: o, handler, routedEvent),
+                state => state.instance.RemoveHandler(state.routedEvent, state.handler));
+        }
+
         /// <summary>
         /// Gets an observable for a <see cref="RoutedEvent{TEventArgs}"/>.
         /// </summary>
@@ -31,7 +54,7 @@ namespace Avalonia.Interactivity
             o = o ?? throw new ArgumentNullException(nameof(o));
             routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
 
-            return Observable.Create<TEventArgs>(x => o.AddHandler(
+            return Observable.Create<TEventArgs>(x => o.AddDisposableHandler(
                 routedEvent, 
                 (_, e) => x.OnNext(e), 
                 routes,

+ 2 - 2
src/Avalonia.Native/WindowImpl.cs

@@ -68,9 +68,9 @@ namespace Avalonia.Native
             _native.CanResize = value;
         }
 
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(Controls.SystemDecorations enabled)
         {
-            _native.HasDecorations = enabled;
+            _native.HasDecorations = (Interop.SystemDecorations)enabled;
         }
 
         public void SetTitleBarColor (Avalonia.Media.Color color)

+ 6 - 3
src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs

@@ -14,6 +14,7 @@ namespace Avalonia.Styling.Activators
     {
         private readonly IList<string> _match;
         private readonly IAvaloniaReadOnlyList<string> _classes;
+        private NotifyCollectionChangedEventHandler? _classesChangedHandler;
 
         public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match)
         {
@@ -21,6 +22,9 @@ namespace Avalonia.Styling.Activators
             _match = match;
         }
 
+        private NotifyCollectionChangedEventHandler ClassesChangedHandler =>
+            _classesChangedHandler ??= ClassesChanged;
+
         public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
         {
             int remainingMatches = toMatch.Count;
@@ -51,16 +55,15 @@ namespace Avalonia.Styling.Activators
             return remainingMatches == 0;
         }
 
-
         protected override void Initialize()
         {
             PublishNext(IsMatching());
-            _classes.CollectionChanged += ClassesChanged;
+            _classes.CollectionChanged += ClassesChangedHandler;
         }
 
         protected override void Deinitialize()
         {
-            _classes.CollectionChanged -= ClassesChanged;
+            _classes.CollectionChanged -= ClassesChangedHandler;
         }
 
         private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e)

+ 96 - 0
src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs

@@ -0,0 +1,96 @@
+using System;
+using Avalonia.Visuals.Media.Imaging;
+
+namespace Avalonia.Media.Imaging
+{
+    /// <summary>
+    /// Crops a Bitmap.
+    /// </summary>
+    public class CroppedBitmap : AvaloniaObject, IImage, IAffectsRender, IDisposable
+    {
+        /// <summary>
+        /// Defines the <see cref="Source"/> property.
+        /// </summary>
+        public static readonly StyledProperty<IImage> SourceProperty =
+            AvaloniaProperty.Register<CroppedBitmap, IImage>(nameof(Source));
+
+        /// <summary>
+        /// Defines the <see cref="SourceRect"/> property.
+        /// </summary>
+        public static readonly StyledProperty<PixelRect> SourceRectProperty =
+            AvaloniaProperty.Register<CroppedBitmap, PixelRect>(nameof(SourceRect));
+
+        public event EventHandler Invalidated;
+
+        static CroppedBitmap()
+        {
+            SourceRectProperty.Changed.AddClassHandler<CroppedBitmap>((x, e) => x.SourceRectChanged(e));
+            SourceProperty.Changed.AddClassHandler<CroppedBitmap>((x, e) => x.SourceChanged(e));
+        }
+
+        /// <summary>
+        /// Gets or sets the source for the bitmap.
+        /// </summary>
+        public IImage Source
+        {
+            get => GetValue(SourceProperty);
+            set => SetValue(SourceProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the rectangular area that the bitmap is cropped to.
+        /// </summary>
+        public PixelRect SourceRect
+        {
+            get => GetValue(SourceRectProperty);
+            set => SetValue(SourceRectProperty, value);
+        }
+
+        public CroppedBitmap()
+        {
+            Source = null;
+            SourceRect = default;
+        }
+
+        public CroppedBitmap(IImage source, PixelRect sourceRect)
+        {
+            Source = source;
+            SourceRect = sourceRect;
+        }
+
+        private void SourceChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.NewValue == null)
+                return;
+            if (!(e.NewValue is IBitmap))
+                throw new ArgumentException("Only IBitmap supported as source");
+            Invalidated?.Invoke(this, e);
+        }
+
+        private void SourceRectChanged(AvaloniaPropertyChangedEventArgs e) => Invalidated?.Invoke(this, e);
+
+        public virtual void Dispose()
+        {
+            (Source as IBitmap)?.Dispose();
+        }
+
+        public Size Size {
+            get
+            {
+                if (Source == null)
+                    return Size.Empty;
+                if (SourceRect.IsEmpty)
+                    return Source.Size;
+                return SourceRect.Size.ToSizeWithDpi((Source as IBitmap).Dpi);
+            }
+        }
+
+        public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
+        {
+            if (Source == null)
+                return;
+            var topLeft = SourceRect.TopLeft.ToPointWithDpi((Source as IBitmap).Dpi);
+            Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode);           
+        }
+    }
+}

+ 18 - 9
src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs

@@ -201,18 +201,17 @@ namespace Avalonia.Media.TextFormatting
             var availableWidth = paragraphWidth;
             var currentWidth = 0.0;
             var runIndex = 0;
+            var length = 0;
 
             while (runIndex < textRuns.Count)
             {
                 var currentRun = textRuns[runIndex];
 
-                currentWidth += currentRun.GlyphRun.Bounds.Width;
-
-                if (currentWidth > availableWidth)
+                if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth)
                 {
-                    var measuredLength = MeasureText(currentRun, paragraphWidth);
+                    var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth);
 
-                    if (measuredLength < text.End)
+                    if (measuredLength < currentRun.Text.Length)
                     {
                         var currentBreakPosition = -1;
 
@@ -241,15 +240,19 @@ namespace Avalonia.Media.TextFormatting
                         }
                     }
 
-                    var splitResult = SplitTextRuns(textRuns, measuredLength);
+                    length += measuredLength;
+
+                    var splitResult = SplitTextRuns(textRuns, length);
 
                     var textLineMetrics =
                         TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment);
 
-                    return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics);
+                    return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics);
                 }
 
-                availableWidth -= currentRun.GlyphRun.Bounds.Width;
+                currentWidth += currentRun.GlyphRun.Bounds.Width;
+
+                length += currentRun.GlyphRun.Characters.Length;
 
                 runIndex++;
             }
@@ -281,12 +284,18 @@ namespace Avalonia.Media.TextFormatting
 
                 if (measuredWidth + advance > availableWidth)
                 {
+                    index--;
                     break;
                 }
 
                 measuredWidth += advance;
             }
 
+            if(index < 0)
+            {
+                return 0;
+            }
+
             var cluster = textRun.GlyphRun.GlyphClusters[index];
 
             var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
@@ -355,7 +364,7 @@ namespace Avalonia.Media.TextFormatting
                     continue;
                 }
 
-                var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i;
+                var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i;
 
                 var first = new ShapedTextRun[firstCount];
 

+ 11 - 5
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@@ -233,6 +233,12 @@ namespace Avalonia.Media.TextFormatting
 
                         var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties);
 
+                        if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight)
+                        {
+                            currentPosition = _text.Length;
+                            break;
+                        }
+
                         UpdateBounds(textLine, ref left, ref right, ref bottom);
 
                         textLines.Add(textLine);
@@ -253,17 +259,17 @@ namespace Avalonia.Media.TextFormatting
                     {
                         var emptyTextLine = CreateEmptyTextLine(currentPosition);
 
+                        if (!double.IsPositiveInfinity(MaxHeight) && bottom + emptyTextLine.LineMetrics.Size.Height > MaxHeight)
+                        {
+                            break;
+                        }
+
                         UpdateBounds(emptyTextLine, ref left, ref right, ref bottom);
 
                         textLines.Add(emptyTextLine);
 
                         break;
                     }
-
-                    if (!double.IsPositiveInfinity(MaxHeight) && MaxHeight < Bounds.Height)
-                    {
-                        break;
-                    }
                 }
 
                 Bounds = new Rect(left, 0, right, bottom);

+ 1 - 0
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@@ -8,6 +8,7 @@ using Avalonia.Metadata;
 [assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
 
 [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]

+ 8 - 2
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -355,15 +355,21 @@ namespace Avalonia.Rendering
 
                     node.BeginRender(context, isLayerRoot);
 
-                    foreach (var operation in node.DrawOperations)
+                    var drawOperations = node.DrawOperations;
+                    var drawOperationsCount = drawOperations.Count;
+                    for (int i = 0; i < drawOperationsCount; i++)
                     {
+                        var operation = drawOperations[i];
                         _currentDraw = operation;
                         operation.Item.Render(context);
                         _currentDraw = null;
                     }
 
-                    foreach (var child in node.Children)
+                    var children = node.Children;
+                    var childrenCount = children.Count;
+                    for (int i = 0; i < childrenCount; i++)
                     {
+                        var child = children[i];
                         Render(context, (VisualNode)child, layer, clipBounds);
                     }
 

+ 11 - 5
src/Avalonia.X11/X11Window.cs

@@ -173,6 +173,7 @@ namespace Avalonia.X11
             
             Surfaces = surfaces.ToArray();
             UpdateMotifHints();
+            UpdateSizeHints(null);
             _xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
                 XNames.XNClientWindow, _handle, IntPtr.Zero);
             XFlush(_x11.Display);
@@ -219,12 +220,16 @@ namespace Avalonia.X11
             var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border |
                               MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH;
 
-            if (_popup || !_systemDecorations)
+            if (_popup || _systemDecorations == SystemDecorations.None)
             {
                 decorations = 0;
             }
+            else if (_systemDecorations == SystemDecorations.BorderOnly)
+            {
+                decorations = MotifDecorations.Border;
+            }
 
-            if (!_canResize)
+            if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
             {
                 functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize);
                 decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH);
@@ -247,7 +252,7 @@ namespace Avalonia.X11
             var min = _minMaxSize.minSize;
             var max = _minMaxSize.maxSize;
 
-            if (!_canResize)
+            if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
                 max = min = _realSize;
             
             if (preResize.HasValue)
@@ -621,7 +626,7 @@ namespace Avalonia.X11
             return rv;
         }
         
-        private bool _systemDecorations = true;
+        private SystemDecorations _systemDecorations = SystemDecorations.Full;
         private bool _canResize = true;
         private const int MaxWindowDimension = 100000;
 
@@ -777,10 +782,11 @@ namespace Avalonia.X11
             (int)(point.X * Scaling + Position.X),
             (int)(point.Y * Scaling + Position.Y));
         
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(SystemDecorations enabled)
         {
             _systemDecorations = enabled;
             UpdateMotifHints();
+            UpdateSizeHints(null);
         }
 
 

+ 10 - 0
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -1298,7 +1298,17 @@ namespace Avalonia.Win32.Interop
         [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)]
         internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, out int finalEffect);
 
+        [DllImport("dwmapi.dll")]
+        public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins);
 
+        [StructLayout(LayoutKind.Sequential)]
+        internal struct MARGINS
+        {
+            public int cxLeftWidth;
+            public int cxRightWidth;
+            public int cyTopHeight;
+            public int cyBottomHeight;
+        }
 
         public enum MONITOR
         {

+ 12 - 8
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -34,7 +34,7 @@ namespace Avalonia.Win32
         private IInputRoot _owner;
         private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock();
         private bool _trackingMouse;
-        private bool _decorated = true;
+        private SystemDecorations _decorated = SystemDecorations.Full;
         private bool _resizable = true;
         private bool _topmost = false;
         private bool _taskbarIcon = true;
@@ -97,7 +97,7 @@ namespace Avalonia.Win32
         {
             get
             {
-                if (_decorated)
+                if (_decorated == SystemDecorations.Full)
                 {
                     var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE);
                     var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE);
@@ -281,7 +281,7 @@ namespace Avalonia.Win32
             UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide);
         }
 
-        public void SetSystemDecorations(bool value)
+        public void SetSystemDecorations(SystemDecorations value)
         {
             if (value == _decorated)
             {
@@ -464,7 +464,7 @@ namespace Avalonia.Win32
                     return IntPtr.Zero;
 
                 case WindowsMessage.WM_NCCALCSIZE:
-                    if (ToInt32(wParam) == 1 && !_decorated)
+                    if (ToInt32(wParam) == 1 && _decorated != SystemDecorations.Full)
                     {
                         return IntPtr.Zero;
                     }
@@ -682,14 +682,14 @@ namespace Avalonia.Win32
                     
                     break;
                 case WindowsMessage.WM_NCPAINT:
-                    if (!_decorated)
+                    if (_decorated != SystemDecorations.Full)
                     {
                         return IntPtr.Zero;
                     }
                     break;
 
                 case WindowsMessage.WM_NCACTIVATE:
-                    if (!_decorated)
+                    if (_decorated != SystemDecorations.Full)
                     {
                         return new IntPtr(1);
                     }
@@ -1001,7 +1001,7 @@ namespace Avalonia.Win32
 
             style |= WindowStyles.WS_OVERLAPPEDWINDOW;
 
-            if (!_decorated)
+            if (_decorated != SystemDecorations.Full)
             {
                 style ^= (WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU);
             }
@@ -1011,6 +1011,10 @@ namespace Avalonia.Win32
                 style ^= (WindowStyles.WS_SIZEFRAME);
             }
 
+            MARGINS margins = new MARGINS();
+            margins.cyBottomHeight = _decorated == SystemDecorations.BorderOnly ? 1 : 0;
+            UnmanagedMethods.DwmExtendFrameIntoClientArea(_hwnd, ref margins);
+
             GetClientRect(_hwnd, out var oldClientRect);
             var oldClientRectOrigin = new UnmanagedMethods.POINT();
             ClientToScreen(_hwnd, ref oldClientRectOrigin);
@@ -1024,7 +1028,7 @@ namespace Avalonia.Win32
             if (oldDecorated != _decorated)
             {
                 var newRect = oldClientRect;
-                if (_decorated)
+                if (_decorated == SystemDecorations.Full)
                     AdjustWindowRectEx(ref newRect, (uint)style, false,
                         GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE));
                 SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height,

+ 1 - 1
src/iOS/Avalonia.iOS/EmbeddableImpl.cs

@@ -20,7 +20,7 @@ namespace Avalonia.iOS
             return Disposable.Empty;
         }
 
-        public void SetSystemDecorations(bool enabled)
+        public void SetSystemDecorations(SystemDecorations enabled)
         {
         }
 

+ 11 - 0
tests/Avalonia.Benchmarks/NullGlyphRun.cs

@@ -0,0 +1,11 @@
+using Avalonia.Platform;
+
+namespace Avalonia.Benchmarks
+{
+    internal class NullGlyphRun : IGlyphRunImpl
+    {
+        public void Dispose()
+        {
+        }
+    }
+}

+ 3 - 1
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@@ -72,7 +72,9 @@ namespace Avalonia.Benchmarks
 
         public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
         {
-            throw new NotImplementedException();
+            width = default;
+
+            return new NullGlyphRun();
         }
     }
 }

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

@@ -385,6 +385,49 @@ namespace Avalonia.Controls.UnitTests
                 Assert.True(target.SelectionEnd <= "123".Length);
             }
         }
+
+        [Fact]
+        public void SelectedText_Changes_OnSelectionChange()
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                var target = new TextBox
+                {
+                    Template = CreateTemplate(),
+                    Text = "0123456789"
+                };
+
+                Assert.True(target.SelectedText == "");
+
+                target.SelectionStart = 2;
+                target.SelectionEnd = 4;
+
+                Assert.True(target.SelectedText == "23");
+            }
+        }
+
+        [Fact]
+        public void SelectedText_EditsText()
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                var target = new TextBox
+                {
+                    Template = CreateTemplate(),
+                    Text = "0123"
+                };
+
+                target.SelectedText = "AA";
+                Assert.True(target.Text == "AA0123");
+
+                target.SelectionStart = 1;
+                target.SelectionEnd = 3;
+                target.SelectedText = "BB";
+
+                Assert.True(target.Text == "ABB123");
+            }
+        }
+
         [Fact]
         public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending()
         {