1
0
Эх сурвалжийг харах

Merge branch 'master' into fix-osx-cursor

Florian 7 жил өмнө
parent
commit
673dc566b3
24 өөрчлөгдсөн 385 нэмэгдсэн , 111 устгасан
  1. 21 5
      src/Avalonia.Base/Collections/AvaloniaListExtensions.cs
  2. 127 0
      src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs
  3. 5 7
      src/Avalonia.Controls/ItemsControl.cs
  4. 6 0
      src/Avalonia.Controls/LayoutTransformControl.cs
  5. 1 1
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  6. 3 23
      src/Avalonia.Controls/ProgressBar.cs
  7. 4 0
      src/Avalonia.Controls/UserControl.cs
  8. 1 1
      src/Avalonia.Controls/Window.cs
  9. 38 20
      src/Avalonia.Themes.Default/ProgressBar.xaml
  10. 5 0
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  11. 3 2
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  12. 1 3
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  13. 3 2
      src/Gtk/Avalonia.Gtk3/FramebufferManager.cs
  14. 6 0
      src/Gtk/Avalonia.Gtk3/Interop/GObject.cs
  15. 14 1
      src/Gtk/Avalonia.Gtk3/Interop/Native.cs
  16. 18 4
      src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs
  17. 5 5
      src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs
  18. 21 12
      src/Shared/PlatformSupport/StandardRuntimePlatform.cs
  19. 6 16
      src/Windows/Avalonia.Win32/WindowFramebuffer.cs
  20. 0 5
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs
  21. 20 0
      tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs
  22. 24 4
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  23. 16 0
      tests/Avalonia.Layout.UnitTests/ArrangeTests.cs
  24. 37 0
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

+ 21 - 5
src/Avalonia.Base/Collections/AvaloniaListExtensions.cs

@@ -34,14 +34,18 @@ namespace Avalonia.Collections
         /// <param name="reset">
         /// An action called when the collection is reset.
         /// </param>
+        /// <param name="weakSubscription">
+        /// Indicates if a weak subscription should be used to track changes to the collection.
+        /// </param>
         /// <returns>A disposable used to terminate the subscription.</returns>
         public static IDisposable ForEachItem<T>(
             this IAvaloniaReadOnlyList<T> collection,
             Action<T> added,
             Action<T> removed,
-            Action reset)
+            Action reset,
+            bool weakSubscription = false)
         {
-            return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset);
+            return collection.ForEachItem((_, i) => added(i), (_, i) => removed(i), reset, weakSubscription);
         }
 
         /// <summary>
@@ -63,12 +67,16 @@ namespace Avalonia.Collections
         /// An action called when the collection is reset. This will be followed by calls to 
         /// <paramref name="added"/> for each item present in the collection after the reset.
         /// </param>
+        /// <param name="weakSubscription">
+        /// Indicates if a weak subscription should be used to track changes to the collection.
+        /// </param>
         /// <returns>A disposable used to terminate the subscription.</returns>
         public static IDisposable ForEachItem<T>(
             this IAvaloniaReadOnlyList<T> collection,
             Action<int, T> added,
             Action<int, T> removed,
-            Action reset)
+            Action reset,
+            bool weakSubscription = false)
         {
             void Add(int index, IList items)
             {
@@ -118,9 +126,17 @@ namespace Avalonia.Collections
             };
 
             Add(0, (IList)collection);
-            collection.CollectionChanged += handler;
 
-            return Disposable.Create(() => collection.CollectionChanged -= handler);
+            if (weakSubscription)
+            {
+                return collection.WeakSubscribe(handler);
+            }
+            else
+            {
+                collection.CollectionChanged += handler;
+
+                return Disposable.Create(() => collection.CollectionChanged -= handler);
+            }
         }
 
         public static IAvaloniaReadOnlyList<TDerived> CreateDerivedList<TSource, TDerived>(

+ 127 - 0
src/Avalonia.Base/Collections/NotifyCollectionChangedExtensions.cs

@@ -0,0 +1,127 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using Avalonia.Utilities;
+
+namespace Avalonia.Collections
+{
+    public static class NotifyCollectionChangedExtensions
+    {
+        /// <summary>
+        /// Gets a weak observable for the CollectionChanged event.
+        /// </summary>
+        /// <param name="collection">The collection.</param>
+        /// <returns>An observable.</returns>
+        public static IObservable<NotifyCollectionChangedEventArgs> GetWeakCollectionChangedObservable(
+            this INotifyCollectionChanged collection)
+        {
+            Contract.Requires<ArgumentNullException>(collection != null);
+
+            return new WeakCollectionChangedObservable(new WeakReference<INotifyCollectionChanged>(collection));
+        }
+
+        /// <summary>
+        /// Subcribes to the CollectionChanged event using a weak subscription.
+        /// </summary>
+        /// <param name="collection">The collection.</param>
+        /// <param name="handler">
+        /// An action called when the collection event is raised.
+        /// </param>
+        /// <returns>A disposable used to terminate the subscription.</returns>
+        public static IDisposable WeakSubscribe(
+            this INotifyCollectionChanged collection, 
+            NotifyCollectionChangedEventHandler handler)
+        {
+            Contract.Requires<ArgumentNullException>(collection != null);
+            Contract.Requires<ArgumentNullException>(handler != null);
+
+            return
+                collection.GetWeakCollectionChangedObservable()
+                          .Subscribe(e => handler.Invoke(collection, e));
+        }
+
+        /// <summary>
+        /// Subcribes to the CollectionChanged event using a weak subscription.
+        /// </summary>
+        /// <param name="collection">The collection.</param>
+        /// <param name="handler">
+        /// An action called when the collection event is raised.
+        /// </param>
+        /// <returns>A disposable used to terminate the subscription.</returns>
+        public static IDisposable WeakSubscribe(
+            this INotifyCollectionChanged collection,
+            Action<NotifyCollectionChangedEventArgs> handler)
+        {
+            Contract.Requires<ArgumentNullException>(collection != null);
+            Contract.Requires<ArgumentNullException>(handler != null);
+
+            return
+                collection.GetWeakCollectionChangedObservable()
+                          .Subscribe(handler);
+        }
+
+        private class WeakCollectionChangedObservable : ObservableBase<NotifyCollectionChangedEventArgs>,
+            IWeakSubscriber<NotifyCollectionChangedEventArgs>
+        {
+            private WeakReference<INotifyCollectionChanged> _sourceReference;
+            private readonly Subject<NotifyCollectionChangedEventArgs> _changed = new Subject<NotifyCollectionChangedEventArgs>();
+
+            private int _count;
+
+            public WeakCollectionChangedObservable(WeakReference<INotifyCollectionChanged> source)
+            {
+                _sourceReference = source;
+            }
+
+            public void OnEvent(object sender, NotifyCollectionChangedEventArgs e)
+            {
+                _changed.OnNext(e);
+            }
+
+            protected override IDisposable SubscribeCore(IObserver<NotifyCollectionChangedEventArgs> observer)
+            {
+                if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
+                {
+                    if (_count++ == 0)
+                    {
+                        WeakSubscriptionManager.Subscribe(
+                            instance,
+                            nameof(instance.CollectionChanged),
+                            this);
+                    }
+
+                    return Observable.Using(() => Disposable.Create(DecrementCount), _ => _changed)
+                        .Subscribe(observer);
+                }
+                else
+                {
+                    _changed.OnCompleted();
+                    observer.OnCompleted();
+                    return Disposable.Empty;
+                }
+            }
+
+            private void DecrementCount()
+            {
+                if (--_count == 0)
+                {
+                    if (_sourceReference.TryGetTarget(out INotifyCollectionChanged instance))
+                    {
+                        WeakSubscriptionManager.Unsubscribe(
+                            instance,
+                            nameof(instance.CollectionChanged),
+                            this);
+                    }
+                }
+            }
+        }
+    }
+}

+ 5 - 7
src/Avalonia.Controls/ItemsControl.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
@@ -54,6 +55,7 @@ namespace Avalonia.Controls
 
         private IEnumerable _items = new AvaloniaList<object>();
         private IItemContainerGenerator _itemContainerGenerator;
+        private IDisposable _itemsCollectionChangedSubscription;
 
         /// <summary>
         /// Initializes static members of the <see cref="ItemsControl"/> class.
@@ -326,12 +328,8 @@ namespace Avalonia.Controls
         /// <param name="e">The event args.</param>
         protected virtual void ItemsChanged(AvaloniaPropertyChangedEventArgs e)
         {
-            var incc = e.OldValue as INotifyCollectionChanged;
-
-            if (incc != null)
-            {
-                incc.CollectionChanged -= ItemsCollectionChanged;
-            }
+            _itemsCollectionChangedSubscription?.Dispose();
+            _itemsCollectionChangedSubscription = null;
 
             var oldValue = e.OldValue as IEnumerable;
             var newValue = e.NewValue as IEnumerable;
@@ -428,7 +426,7 @@ namespace Avalonia.Controls
 
             if (incc != null)
             {
-                incc.CollectionChanged += ItemsCollectionChanged;
+                _itemsCollectionChangedSubscription = incc.WeakSubscribe(ItemsCollectionChanged);
             }
         }
 

+ 6 - 0
src/Avalonia.Controls/LayoutTransformControl.cs

@@ -15,6 +15,9 @@ using System.Reactive.Linq;
 
 namespace Avalonia.Controls
 {
+    /// <summary>
+    /// Control that implements support for transformations as if applied by LayoutTransform.
+    /// </summary>
     public class LayoutTransformControl : ContentControl
     {
         public static readonly AvaloniaProperty<Transform> LayoutTransformProperty =
@@ -26,6 +29,9 @@ namespace Avalonia.Controls
                 .AddClassHandler<LayoutTransformControl>(x => x.OnLayoutTransformChanged);
         }
 
+        /// <summary>
+        /// Gets or sets a graphics transformation that should apply to this element when layout is performed.
+        /// </summary>
         public Transform LayoutTransform
         {
             get { return GetValue(LayoutTransformProperty); }

+ 1 - 1
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@@ -224,7 +224,7 @@ namespace Avalonia.Controls.Presenters
                 CanVerticallyScroll ? Math.Max(Child.DesiredSize.Height, finalSize.Height) : finalSize.Height);
             ArrangeOverrideImpl(size, -Offset);
             Viewport = finalSize;
-            Extent = Child.Bounds.Size;
+            Extent = Child.Bounds.Size.Inflate(Child.Margin);
             return finalSize;
         }
 

+ 3 - 23
src/Avalonia.Controls/ProgressBar.cs

@@ -26,12 +26,13 @@ namespace Avalonia.Controls
 
         static ProgressBar()
         {
+            PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical");
+            PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal");
+
             ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged);
 
             IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(
                 (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); });
-            OrientationProperty.Changed.AddClassHandler<ProgressBar>(
-                (p, e) => { if (p._indicator != null) p.UpdateOrientation((Orientation)e.NewValue); });
         }
 
         public bool IsIndeterminate
@@ -59,7 +60,6 @@ namespace Avalonia.Controls
             _indicator = e.NameScope.Get<Border>("PART_Indicator");
 
             UpdateIndicator(Bounds.Size);
-            UpdateOrientation(Orientation);
             UpdateIsIndeterminate(IsIndeterminate);
         }
 
@@ -86,26 +86,6 @@ namespace Avalonia.Controls
             }
         }
 
-        private void UpdateOrientation(Orientation orientation)
-        {
-            if (orientation == Orientation.Horizontal)
-            {
-                MinHeight = 14;
-                MinWidth = 200;
-
-                _indicator.HorizontalAlignment = HorizontalAlignment.Left;
-                _indicator.VerticalAlignment = VerticalAlignment.Stretch;
-            }
-            else
-            {
-                MinHeight = 200;
-                MinWidth = 14;
-
-                _indicator.HorizontalAlignment = HorizontalAlignment.Stretch;
-                _indicator.VerticalAlignment = VerticalAlignment.Bottom;
-            }
-        }
-
         private void UpdateIsIndeterminate(bool isIndeterminate)
         {
             if (isIndeterminate)

+ 4 - 0
src/Avalonia.Controls/UserControl.cs

@@ -6,6 +6,9 @@ using Avalonia.Styling;
 
 namespace Avalonia.Controls
 {
+    /// <summary>
+    /// Provides the base class for defining a new control that encapsulates related existing controls and provides its own logic.
+    /// </summary>
     public class UserControl : ContentControl, IStyleable, INameScope
     {
         private readonly NameScope _nameScope = new NameScope();
@@ -24,6 +27,7 @@ namespace Avalonia.Controls
             remove { _nameScope.Unregistered -= value; }
         }
 
+        /// <inheritdoc/>
         Type IStyleable.StyleKey => typeof(ContentControl);
 
         /// <inheritdoc/>

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

@@ -358,7 +358,7 @@ namespace Avalonia.Controls
                         modal?.Dispose();
                         SetIsEnabled(affectedWindows, true);
                         activated?.Activate();
-                        result.SetResult((TResult)_dialogResult);
+                        result.SetResult((TResult)(_dialogResult ?? default(TResult)));
                     });
 
                 return result.Task;

+ 38 - 20
src/Avalonia.Themes.Default/ProgressBar.xaml

@@ -1,20 +1,38 @@
-<Style xmlns="https://github.com/avaloniaui" Selector="ProgressBar">
-  <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
-  <Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}"/>
-  <Setter Property="Template">
-    <ControlTemplate>
-      <Border Background="{TemplateBinding Background}"
-              BorderBrush="{TemplateBinding BorderBrush}"
-              BorderThickness="{TemplateBinding BorderThickness}">
-        <Grid>
-          <Border Name="PART_Track"
-                  BorderThickness="1"
-                  BorderBrush="{TemplateBinding Background}"/>
-          <Border Name="PART_Indicator"
-                  BorderThickness="1"
-                  Background="{TemplateBinding Foreground}"/>
-        </Grid>
-      </Border>
-    </ControlTemplate>
-  </Setter>
-</Style>
+<Styles xmlns="https://github.com/avaloniaui">
+  <Style Selector="ProgressBar">
+    <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
+    <Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}"/> 
+    <Setter Property="Template">
+      <ControlTemplate>
+        <Border Background="{TemplateBinding Background}"
+                BorderBrush="{TemplateBinding BorderBrush}"
+                BorderThickness="{TemplateBinding BorderThickness}">
+          <Grid>
+            <Border Name="PART_Track"
+                    BorderThickness="1"
+                    BorderBrush="{TemplateBinding Background}"/>
+            <Border Name="PART_Indicator"
+                    BorderThickness="1"
+                    Background="{TemplateBinding Foreground}" />
+          </Grid>
+        </Border>
+      </ControlTemplate>
+    </Setter>
+  </Style>
+  <Style Selector="ProgressBar:horizontal /template/ Border#PART_Indicator">
+    <Setter Property="HorizontalAlignment" Value="Left"/>
+    <Setter Property="VerticalAlignment" Value="Stretch"/>
+  </Style>
+  <Style Selector="ProgressBar:vertical /template/ Border#PART_Indicator">
+    <Setter Property="HorizontalAlignment" Value="Stretch"/>
+    <Setter Property="VerticalAlignment" Value="Bottom"/>
+  </Style>
+  <Style Selector="ProgressBar:horizontal">
+    <Setter Property="MinWidth" Value="200"/>
+    <Setter Property="MinHeight" Value="14"/>
+  </Style>
+  <Style Selector="ProgressBar:vertical">
+    <Setter Property="MinWidth" Value="14"/>
+    <Setter Property="MinHeight" Value="200"/>
+  </Style>
+</Styles>

+ 5 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@@ -69,6 +69,11 @@ namespace Avalonia.Rendering.SceneGraph
         /// </summary>
         IReadOnlyList<IRef<IDrawOperation>> DrawOperations { get; }
 
+        /// <summary>
+        /// Gets the opacity of the scene graph node.
+        /// </summary>
+        double Opacity { get; }
+
         /// <summary>
         /// Sets up the drawing context for rendering the node's geometry.
         /// </summary>

+ 3 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -170,8 +170,9 @@ namespace Avalonia.Rendering.SceneGraph
                     var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip);
 
                     forceRecurse = forceRecurse ||
-                        node.Transform != contextImpl.Transform ||
-                        node.ClipBounds != clipBounds;
+                        node.ClipBounds != clipBounds ||
+                        node.Opacity != opacity ||
+                        node.Transform != contextImpl.Transform;
 
                     node.Transform = contextImpl.Transform;
                     node.ClipBounds = clipBounds;

+ 1 - 3
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -67,9 +67,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <inheritdoc/>
         public bool HasAncestorGeometryClip { get; }
 
-        /// <summary>
-        /// Gets or sets the opacity of the scene graph node.
-        /// </summary>
+        /// <inheritdoc/>
         public double Opacity
         {
             get { return _opacity; }

+ 3 - 2
src/Gtk/Avalonia.Gtk3/FramebufferManager.cs

@@ -26,8 +26,9 @@ namespace Avalonia.Gtk3
         {
             // This method may be called from non-UI thread, don't touch anything that calls back to GTK/GDK
             var s = _window.ClientSize;
-            var width = (int) s.Width;
-            var height = (int) s.Height;
+            var width = Math.Max(1, (int) s.Width);
+            var height = Math.Max(1, (int) s.Height);
+            
             
             if (!Dispatcher.UIThread.CheckAccess() && Gtk3Platform.DisplayClassName.ToLower().Contains("x11"))
             {

+ 6 - 0
src/Gtk/Avalonia.Gtk3/Interop/GObject.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.Runtime.InteropServices;
 using System.Text;
@@ -21,7 +22,12 @@ namespace Avalonia.Gtk3.Interop
         protected override bool ReleaseHandle()
         {
             if (handle != IntPtr.Zero)
+            {
+                Debug.Assert(Native.GTypeCheckInstanceIsFundamentallyA(handle, new IntPtr(Native.G_TYPE_OBJECT)),
+                    "Handle is not a GObject");
                 Native.GObjectUnref(handle);
+            }
+
             handle = IntPtr.Zero;
             return true;
         }

+ 14 - 1
src/Gtk/Avalonia.Gtk3/Interop/Native.cs

@@ -219,7 +219,10 @@ namespace Avalonia.Gtk3.Interop
             public delegate void gtk_widget_queue_draw_area(GtkWidget widget, int x, int y, int width, int height);
             
             [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)]
-            public delegate void gtk_widget_add_tick_callback(GtkWidget widget, TickCallback callback, IntPtr userData, IntPtr destroy);
+            public delegate uint gtk_widget_add_tick_callback(GtkWidget widget, TickCallback callback, IntPtr userData, IntPtr destroy);
+            
+            [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)]
+            public delegate uint gtk_widget_remove_tick_callback(GtkWidget widget, uint id);
 
             [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)]
             public delegate GtkImContext gtk_im_multicontext_new();
@@ -256,6 +259,9 @@ namespace Avalonia.Gtk3.Interop
 
             [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)]
             public delegate void gtk_window_unmaximize(GtkWindow window);
+            
+            [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)]
+            public delegate void gtk_window_close(GtkWindow window);
 
             [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)]
             public delegate void gtk_window_set_geometry_hints(GtkWindow window, IntPtr geometry_widget, ref GdkGeometry geometry, GdkWindowHints geom_mask);
@@ -341,6 +347,9 @@ namespace Avalonia.Gtk3.Interop
 
             [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Glib)]
             public delegate ulong g_free(IntPtr data);
+
+            [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)]
+            public delegate bool g_type_check_instance_is_fundamentally_a(IntPtr instance, IntPtr type);
             
             [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Glib)]
             public unsafe delegate void g_slist_free(GSList* data);
@@ -427,6 +436,7 @@ namespace Avalonia.Gtk3.Interop
         public static D.g_timeout_add GTimeoutAdd;
         public static D.g_timeout_add_full GTimeoutAddFull;
         public static D.g_free GFree;
+        public static D.g_type_check_instance_is_fundamentally_a GTypeCheckInstanceIsFundamentallyA;
         public static D.g_slist_free GSlistFree;
         public static D.g_memory_input_stream_new_from_data GMemoryInputStreamNewFromData;
         public static D.gtk_widget_set_double_buffered GtkWidgetSetDoubleBuffered;
@@ -434,6 +444,7 @@ namespace Avalonia.Gtk3.Interop
         public static D.gdk_window_invalidate_rect GdkWindowInvalidateRect;
         public static D.gtk_widget_queue_draw_area GtkWidgetQueueDrawArea;
         public static D.gtk_widget_add_tick_callback GtkWidgetAddTickCallback;
+        public static D.gtk_widget_remove_tick_callback GtkWidgetRemoveTickCallback;
         public static D.gtk_widget_activate GtkWidgetActivate;
         public static D.gtk_clipboard_get_for_display GtkClipboardGetForDisplay;
         public static D.gtk_clipboard_request_text GtkClipboardRequestText;
@@ -456,6 +467,7 @@ namespace Avalonia.Gtk3.Interop
         public static D.gtk_window_deiconify GtkWindowDeiconify;
         public static D.gtk_window_maximize GtkWindowMaximize;
         public static D.gtk_window_unmaximize GtkWindowUnmaximize;
+        public static D.gtk_window_close GtkWindowClose;
         public static D.gdk_window_begin_move_drag GdkWindowBeginMoveDrag;
         public static D.gdk_window_begin_resize_drag GdkWindowBeginResizeDrag;
         public static D.gdk_event_request_motions GdkEventRequestMotions;
@@ -490,6 +502,7 @@ namespace Avalonia.Gtk3.Interop
         public static D.cairo_set_font_size CairoSetFontSize;
         public static D.cairo_move_to CairoMoveTo;
         public static D.cairo_destroy CairoDestroy;
+        public const int G_TYPE_OBJECT = 80;
     }
 
     public enum GtkWindowType

+ 18 - 4
src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs

@@ -33,11 +33,11 @@ namespace Avalonia.Gtk3
         private readonly AutoResetEvent _canSetNextOperation = new AutoResetEvent(true);
         internal IntPtr? GdkWindowHandle;
         private bool _overrideRedirect;
+        private uint? _tickCallback;
         public WindowBaseImpl(GtkWindow gtkWidget)
         {
             
             GtkWidget = gtkWidget;
-            Disposables.Add(gtkWidget);
             _framebuffer = new FramebufferManager(this);
             _imContext = Native.GtkImMulticontextNew();
             Disposables.Add(_imContext);
@@ -62,7 +62,7 @@ namespace Avalonia.Gtk3
             {
                 Native.GtkWidgetSetDoubleBuffered(gtkWidget, false);
                 _gcHandle = GCHandle.Alloc(this);
-                Native.GtkWidgetAddTickCallback(GtkWidget, PinnedStaticCallback, GCHandle.ToIntPtr(_gcHandle), IntPtr.Zero);
+                _tickCallback = Native.GtkWidgetAddTickCallback(GtkWidget, PinnedStaticCallback, GCHandle.ToIntPtr(_gcHandle), IntPtr.Zero);
                 
             }
         }
@@ -103,7 +103,7 @@ namespace Avalonia.Gtk3
 
         private bool OnDestroy(IntPtr gtkwidget, IntPtr userdata)
         {
-            Dispose();
+            DoDispose(true);
             return false;
         }
 
@@ -297,14 +297,28 @@ namespace Avalonia.Gtk3
         }
 
 
-        public void Dispose()
+        public void Dispose() => DoDispose(false);
+        
+        void DoDispose(bool fromDestroy)
         {
+            if (_tickCallback.HasValue)
+            {
+                if (!GtkWidget.IsClosed)
+                    Native.GtkWidgetRemoveTickCallback(GtkWidget, _tickCallback.Value);
+                _tickCallback = null;
+            }
+            
             //We are calling it here, since signal handler will be detached
             if (!GtkWidget.IsClosed)
                 Closed?.Invoke();
             foreach(var d in Disposables.AsEnumerable().Reverse())
                 d.Dispose();
             Disposables.Clear();
+            
+            if (!fromDestroy && !GtkWidget.IsClosed)
+                Native.GtkWindowClose(GtkWidget);
+            GtkWidget.Dispose();
+            
             if (_gcHandle.IsAllocated)
             {
                 _gcHandle.Free();

+ 5 - 5
src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs

@@ -12,6 +12,7 @@ namespace Avalonia.MonoMac
         private readonly TopLevelImpl.TopLevelView _view;
         private readonly CGSize _logicalSize;
         private readonly bool _isDeferred;
+        private readonly IUnmanagedBlob _blob;
 
         [DllImport("libc")]
         static extern void memset(IntPtr p, int c, IntPtr size);
@@ -29,13 +30,13 @@ namespace Avalonia.MonoMac
             Dpi = new Vector(96 * pixelSize.Width / _logicalSize.Width, 96 * pixelSize.Height / _logicalSize.Height);
             Format = PixelFormat.Rgba8888;
             var size = Height * RowBytes;
-            Address = Marshal.AllocHGlobal(size);
+            _blob = AvaloniaLocator.Current.GetService<IRuntimePlatform>().AllocBlob(size);
             memset(Address, 0, new IntPtr(size));
         }
         
         public void Dispose()
         {
-            if (Address == IntPtr.Zero)
+            if (_blob.IsDisposed)
                 return;
             var nfo = (int) CGBitmapFlags.ByteOrder32Big | (int) CGImageAlphaInfo.PremultipliedLast;
             CGImage image = null;
@@ -71,14 +72,13 @@ namespace Avalonia.MonoMac
                     else
                         _view.SetBackBufferImage(new SavedImage(image, _logicalSize));
                 }
-                Marshal.FreeHGlobal(Address);
-                Address = IntPtr.Zero;
+                _blob.Dispose();
             }
 
 
         }
 
-        public IntPtr Address { get; private set; }
+        public IntPtr Address => _blob.Address;
         public int Width { get; }
         public int Height { get; }
         public int RowBytes { get; }

+ 21 - 12
src/Shared/PlatformSupport/StandardRuntimePlatform.cs

@@ -28,11 +28,13 @@ namespace Avalonia.Shared.PlatformSupport
         class UnmanagedBlob : IUnmanagedBlob
         {
             private readonly StandardRuntimePlatform _plat;
+            private IntPtr _address;
+            private readonly object _lock = new object();
 #if DEBUG
             private static readonly List<string> Backtraces = new List<string>();
             private static Thread GCThread;
             private readonly string _backtrace;
-
+            private static readonly object _btlock = new object();
 
             class GCThreadDetector
             {
@@ -55,28 +57,35 @@ namespace Avalonia.Shared.PlatformSupport
             
             public UnmanagedBlob(StandardRuntimePlatform plat, int size)
             {
+                if (size <= 0)
+                    throw new ArgumentException("Positive number required", nameof(size));
                 _plat = plat;
-                Address = plat.Alloc(size);
+                _address = plat.Alloc(size);
                 GC.AddMemoryPressure(size);
                 Size = size;
 #if DEBUG
                 _backtrace = Environment.StackTrace;
-                Backtraces.Add(_backtrace);
+                lock (_btlock)
+                    Backtraces.Add(_backtrace);
 #endif
             }
 
             void DoDispose()
             {
-                if (!IsDisposed)
+                lock (_lock)
                 {
+                    if (!IsDisposed)
+                    {
 #if DEBUG
-                    Backtraces.Remove(_backtrace);
+                        lock (_btlock)
+                            Backtraces.Remove(_backtrace);
 #endif
-                    _plat.Free(Address, Size);
-                    GC.RemoveMemoryPressure(Size);
-                    IsDisposed = true;
-                    Address = IntPtr.Zero;
-                    Size = 0;
+                        _plat.Free(_address, Size);
+                        GC.RemoveMemoryPressure(Size);
+                        IsDisposed = true;
+                        _address = IntPtr.Zero;
+                        Size = 0;
+                    }
                 }
             }
 
@@ -102,7 +111,7 @@ namespace Avalonia.Shared.PlatformSupport
                 DoDispose();
             }
 
-            public IntPtr Address { get; private set; }
+            public IntPtr Address => IsDisposed ? throw new ObjectDisposedException("UnmanagedBlob") : _address; 
             public int Size { get; private set; }
             public bool IsDisposed { get; private set; }
         }
@@ -155,4 +164,4 @@ namespace Avalonia.Shared.PlatformSupport
         void Free(IntPtr ptr, int len) => Marshal.FreeHGlobal(ptr);
 #endif
     }
-}
+}

+ 6 - 16
src/Windows/Avalonia.Win32/WindowFramebuffer.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Win32
     public class WindowFramebuffer : ILockedFramebuffer
     {
         private readonly IntPtr _handle;
-        private IntPtr _pBitmap;
+        private IUnmanagedBlob _bitmapBlob;
         private UnmanagedMethods.BITMAPINFOHEADER _bmpInfo;
 
         public WindowFramebuffer(IntPtr handle, int width, int height)
@@ -27,7 +27,7 @@ namespace Avalonia.Win32
             _bmpInfo.Init();
             _bmpInfo.biWidth = width;
             _bmpInfo.biHeight = -height;
-            _pBitmap = Marshal.AllocHGlobal(width * height * 4);
+            _bitmapBlob = AvaloniaLocator.Current.GetService<IRuntimePlatform>().AllocBlob(width * height * 4);
         }
 
         ~WindowFramebuffer()
@@ -35,7 +35,7 @@ namespace Avalonia.Win32
             Deallocate();
         }
 
-        public IntPtr Address => _pBitmap;
+        public IntPtr Address => _bitmapBlob.Address;
         public int RowBytes => Width * 4;
         public PixelFormat Format => PixelFormat.Bgra8888;
 
@@ -70,21 +70,18 @@ namespace Avalonia.Win32
         public void DrawToDevice(IntPtr hDC, int destX = 0, int destY = 0, int srcX = 0, int srcY = 0, int width = -1,
             int height = -1)
         {
-            if(_pBitmap == IntPtr.Zero)
-                throw new ObjectDisposedException("Framebuffer");
             if (width == -1)
                 width = Width;
             if (height == -1)
                 height = Height;
             UnmanagedMethods.SetDIBitsToDevice(hDC, destX, destY, (uint) width, (uint) height, srcX, srcY,
-                0, (uint)Height, _pBitmap, ref _bmpInfo, 0);
+                0, (uint)Height, _bitmapBlob.Address, ref _bmpInfo, 0);
         }
 
         public bool DrawToWindow(IntPtr hWnd, int destX = 0, int destY = 0, int srcX = 0, int srcY = 0, int width = -1,
             int height = -1)
         {
-
-            if (_pBitmap == IntPtr.Zero)
+            if (_bitmapBlob.IsDisposed)
                 throw new ObjectDisposedException("Framebuffer");
             if (hWnd == IntPtr.Zero)
                 return false;
@@ -102,13 +99,6 @@ namespace Avalonia.Win32
             DrawToWindow(_handle);
         }
 
-        public void Deallocate()
-        {
-            if (_pBitmap != IntPtr.Zero)
-            {
-                Marshal.FreeHGlobal(_pBitmap);
-                _pBitmap = IntPtr.Zero;
-            }
-        }
+        public void Deallocate() => _bitmapBlob.Dispose();
     }
 }

+ 0 - 5
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Layout.cs

@@ -185,10 +185,5 @@ namespace Avalonia.Controls.UnitTests.Presenters
 
             Assert.Equal(new Rect(84, 0, 16, 16), content.Bounds);
         }
-
-        [Fact]
-        public void Padding_Is_Applied_To_TopLeft_Aligned_Content()
-        {
-        }
     }
 }

+ 20 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ScrollContentPresenterTests.cs

@@ -223,6 +223,26 @@ namespace Avalonia.Controls.UnitTests.Presenters
             Assert.Equal(100, child.Bounds.Width);
         }
 
+        [Fact]
+        public void Extent_Should_Include_Content_Margin()
+        {
+            var target = new ScrollContentPresenter
+            {
+                Content = new Border
+                {
+                    Width = 100,
+                    Height = 100,
+                    Margin = new Thickness(5),
+                }
+            };
+
+            target.UpdateChild();
+            target.Measure(new Size(50, 50));
+            target.Arrange(new Rect(0, 0, 50, 50));
+
+            Assert.Equal(new Size(110, 110), target.Extent);
+        }
+
         [Fact]
         public void Extent_Width_Should_Be_Arrange_Width_When_CanScrollHorizontally_False()
         {

+ 24 - 4
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -6,6 +6,7 @@
 
 using System;
 using System.Collections.Generic;
+using System.Threading.Tasks;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.UnitTests;
@@ -231,11 +232,23 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
-        private void ClearOpenWindows()
+        [Fact]
+        public async Task ShowDialog_With_ValueType_Returns_Default_When_Closed()
         {
-            // HACK: We really need a decent way to have "statics" that can be scoped to
-            // AvaloniaLocator scopes.
-            ((IList<Window>)Window.OpenWindows).Clear();
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var windowImpl = new Mock<IWindowImpl>();
+                windowImpl.SetupProperty(x => x.Closed);
+                windowImpl.Setup(x => x.Scaling).Returns(1);
+
+                var target = new Window(windowImpl.Object);
+                var task = target.ShowDialog<bool>();
+
+                windowImpl.Object.Closed();
+
+                var result = await task;
+                Assert.False(result);
+            }
         }
 
         [Fact]
@@ -321,5 +334,12 @@ namespace Avalonia.Controls.UnitTests
                 x.Scaling == 1 &&
                 x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer.Object);
         }
+
+        private void ClearOpenWindows()
+        {
+            // HACK: We really need a decent way to have "statics" that can be scoped to
+            // AvaloniaLocator scopes.
+            ((IList<Window>)Window.OpenWindows).Clear();
+        }
     }
 }

+ 16 - 0
tests/Avalonia.Layout.UnitTests/ArrangeTests.cs

@@ -8,6 +8,22 @@ namespace Avalonia.Layout.UnitTests
 {
     public class ArrangeTests
     {
+        [Fact]
+        public void Bounds_Should_Not_Include_Margin()
+        {
+            var target = new Decorator
+            {
+                Width = 100,
+                Height = 100,
+                Margin = new Thickness(5),
+            };
+
+            Assert.False(target.IsMeasureValid);
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(target.DesiredSize));
+            Assert.Equal(new Rect(5, 5, 100, 100), target.Bounds);
+        }
+
         [Fact]
         public void Margin_Should_Be_Subtracted_From_Arrange_FinalSize()
         {

+ 37 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/SceneBuilderTests.cs

@@ -656,6 +656,43 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
             }
         }
 
+        [Fact]
+        public void Setting_Opacity_Should_Add_Descendent_Bounds_To_DirtyRects()
+        {
+            using (TestApplication())
+            {
+                Decorator decorator;
+                Border border;
+                var tree = new TestRoot
+                {
+                    Child = decorator = new Decorator
+                    {
+                        Child = border = new Border
+                        {
+                            Background = Brushes.Red,
+                            Width = 100,
+                            Height = 100,
+                        }
+                    }
+                };
+
+                tree.Measure(Size.Infinity);
+                tree.Arrange(new Rect(tree.DesiredSize));
+
+                var scene = new Scene(tree);
+                var sceneBuilder = new SceneBuilder();
+                sceneBuilder.UpdateAll(scene);
+
+                decorator.Opacity = 0.5;
+                scene = scene.CloneScene();
+                sceneBuilder.Update(scene, decorator);
+
+                Assert.NotEmpty(scene.Layers.Single().Dirty);
+                var dirty = scene.Layers.Single().Dirty.Single();
+                Assert.Equal(new Rect(0, 0, 100, 100), dirty);
+            }
+        }
+
         [Fact]
         public void Should_Set_GeometryClip()
         {