diff --git a/appveyor.yml b/appveyor.yml index cef8c4a2cc..aa8c19ace4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -16,9 +16,7 @@ environment: init: - ps: if (Test-Path env:nuget_address) {[System.IO.File]::AppendAllText("C:\Windows\System32\drivers\etc\hosts", "`n$($env:nuget_address)`tapi.nuget.org")} install: - - if not exist gtk-sharp-2.12.26.msi appveyor DownloadFile http://download.xamarin.com/GTKforWindows/Windows/gtk-sharp-2.12.26.msi - if not exist dotnet-2.0.0.exe appveyor DownloadFile https://download.microsoft.com/download/0/F/D/0FD852A4-7EA1-4E2A-983A-0484AC19B92C/dotnet-sdk-2.0.0-win-x64.exe -FileName "dotnet-2.0.0.exe" - - ps: Start-Process -FilePath "msiexec" -ArgumentList "/i gtk-sharp-2.12.26.msi /quiet /qn /norestart" -Wait - ps: Start-Process -FilePath "dotnet-2.0.0.exe" -ArgumentList "/quiet" -Wait - cmd: set PATH=%programfiles(x86)%\GtkSharp\2.12\bin\;%PATH% before_build: @@ -36,5 +34,4 @@ artifacts: - path: artifacts\zip\*.zip - path: artifacts\inspectcode.xml cache: - - gtk-sharp-2.12.26.msi - dotnet-2.0.0.exe diff --git a/build/NetCore.props b/build/NetCore.props index cebb02c7f5..b9cde28015 100644 --- a/build/NetCore.props +++ b/build/NetCore.props @@ -1,6 +1,4 @@  - - diff --git a/docs/guidelines/build.md b/docs/guidelines/build.md index 828a4ddab3..559790b197 100644 --- a/docs/guidelines/build.md +++ b/docs/guidelines/build.md @@ -4,12 +4,6 @@ Avalonia requires at least Visual Studio 2017 and .NET Core SDK 2.0 to build on Windows. -### Install GTK Sharp - -For the moment under windows, you must have [gtk-sharp](http://www.mono-project.com/download/#download-win) -installed. Note that after installing the package your machine may require a restart before GTK# is -added to your path. We hope to remove or make this dependency optional at some point in the future. - ### Clone the Avalonia repository ``` diff --git a/readme.md b/readme.md index 9f16405726..3f4840fce2 100644 --- a/readme.md +++ b/readme.md @@ -7,7 +7,7 @@ A multi-platform .NET UI framework. It can run on Windows, Linux, Mac OS X, iOS and Android. -![](docs/images/screen.png) +[![](docs/images/screen.png)](https://youtu.be/wHcB3sGLVYg) Desktop platforms: @@ -36,7 +36,7 @@ Try out the ControlCatalog to give it a quick demo. Avalonia is a multi-platform windowing toolkit - somewhat like WPF - that is intended to be multi- platform. It supports XAML, lookless controls and a flexible styling system, and runs on Windows -using Direct2D and other operating systems using Gtk & Cairo. +using Direct2D and other operating systems using Skia and OS-specific windowing backend (GTK, Cocoa, etc). ## Current Status diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml index 5cf7fee4d1..aa7d60bd11 100644 --- a/samples/ControlCatalog/Pages/ToolTipPage.xaml +++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml @@ -10,7 +10,7 @@ HorizontalAlignment="Center"> @@ -24,7 +24,7 @@ diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs index e1a09f094d..66253dc5b2 100644 --- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs +++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs @@ -14,6 +14,15 @@ namespace Avalonia.Platform IDisposable StartSystemTimer(TimeSpan interval, Action tick); string GetStackTrace(); RuntimePlatformInfo GetRuntimeInfo(); + IUnmanagedBlob AllocBlob(int size); + } + + public interface IUnmanagedBlob : IDisposable + { + IntPtr Address { get; } + int Size { get; } + bool IsDisposed { get; } + } public struct RuntimePlatformInfo diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 0e12fb3283..a6fe35d668 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -170,6 +170,15 @@ namespace Avalonia.Controls } } + if (Orientation == Orientation.Vertical) + { + measuredHeight -= gap; + } + else + { + measuredWidth -= gap; + } + return new Size(measuredWidth, measuredHeight); } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 5d1b9a1462..079e571d29 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -253,9 +253,7 @@ namespace Avalonia.Controls if (AutoScrollToSelectedItem) { - DispatcherTimer.RunOnce( - container.ContainerControl.BringIntoView, - TimeSpan.Zero); + Dispatcher.UIThread.InvokeAsync(container.ContainerControl.BringIntoView); } break; diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj index eade213c4c..53b2c997d0 100644 --- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj +++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj @@ -2,6 +2,7 @@ netcoreapp2.0 false + $(DefineConstants);DOTNETCORE bin\$(Configuration)\Avalonia.DotNetCoreRuntime.XML @@ -21,5 +22,5 @@ - + \ No newline at end of file diff --git a/src/Avalonia.DotNetCoreRuntime/NetCoreRuntimePlatform.cs b/src/Avalonia.DotNetCoreRuntime/NetCoreRuntimePlatform.cs deleted file mode 100644 index 6aab8e0243..0000000000 --- a/src/Avalonia.DotNetCoreRuntime/NetCoreRuntimePlatform.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Microsoft.DotNet.PlatformAbstractions; -using Microsoft.Extensions.DependencyModel; - -namespace Avalonia.Shared.PlatformSupport -{ - internal partial class StandardRuntimePlatform - { - private static readonly Lazy Assemblies = new Lazy(LoadAssemblies); - public Assembly[] GetLoadedAssemblies() => Assemblies.Value; - - static Assembly[] LoadAssemblies() - { - var assemblies = new List(); - // Mostly copy-pasted from (MIT): - // https://github.com/StefH/System.AppDomain.Core/blob/0b35e676c2721aa367b96e62eb52c97ee0b43a70/src/System.AppDomain.NetCoreApp/AppDomain.cs - - foreach (var assemblyName in - DependencyContext.Default.GetRuntimeAssemblyNames(RuntimeEnvironment.GetRuntimeIdentifier())) - { - try - { - var assembly = Assembly.Load(assemblyName); - // just load all types and skip this assembly if one or more types cannot be resolved - assembly.DefinedTypes.ToArray(); - assemblies.Add(assembly); - } - catch (Exception ex) - { - Debug.Write(ex.Message); - } - } - return assemblies.ToArray(); - } - } -} diff --git a/src/Avalonia.Themes.Default/RepeatButton.xaml b/src/Avalonia.Themes.Default/RepeatButton.xaml index 557e2d3b03..1caaa266de 100644 --- a/src/Avalonia.Themes.Default/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/RepeatButton.xaml @@ -1,13 +1,13 @@ \ No newline at end of file diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs index 59a895a22f..f56e7448a7 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using Avalonia.Media; -using Avalonia.Platform; using Avalonia.VisualTree; namespace Avalonia.Rendering.SceneGraph @@ -12,20 +11,16 @@ namespace Avalonia.Rendering.SceneGraph /// /// Base class for draw operations that can use a brush. /// - internal abstract class BrushDrawOperation : IDrawOperation + internal abstract class BrushDrawOperation : DrawOperation { - /// - public abstract Rect Bounds { get; } - - /// - public abstract bool HitTest(Point p); + public BrushDrawOperation(Rect bounds, Matrix transform, Pen pen) + : base(bounds, transform, pen) + { + } /// /// Gets a collection of child scenes that are needed to draw visual brushes. /// public abstract IDictionary ChildScenes { get; } - - /// - public abstract void Render(IDrawingContextImpl context); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs new file mode 100644 index 0000000000..4c6ed189ff --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs @@ -0,0 +1,26 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// Base class for draw operations that have bounds. + /// + internal abstract class DrawOperation : IDrawOperation + { + public DrawOperation(Rect bounds, Matrix transform, Pen pen) + { + bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform); + Bounds = new Rect( + new Point(Math.Floor(bounds.X), Math.Floor(bounds.Y)), + new Point(Math.Ceiling(bounds.Right), Math.Ceiling(bounds.Bottom))); + } + + public Rect Bounds { get; } + + public abstract bool HitTest(Point p); + + public abstract void Render(IDrawingContextImpl context); + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs index b884c42d99..6310122183 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs @@ -28,8 +28,8 @@ namespace Avalonia.Rendering.SceneGraph Pen pen, IGeometryImpl geometry, IDictionary childScenes = null) + : base(geometry.GetRenderBounds(pen?.Thickness ?? 0), transform, null) { - Bounds = geometry.GetRenderBounds(pen?.Thickness ?? 0).TransformToAABB(transform); Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); @@ -37,9 +37,6 @@ namespace Avalonia.Rendering.SceneGraph ChildScenes = childScenes; } - /// - public override Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs index 8c3bb72463..839fd9b0e5 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IDrawOperation.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph public interface IDrawOperation { /// - /// Gets the bounds of the visible content in the node. + /// Gets the bounds of the visible content in the node in global coordinates. /// Rect Bounds { get; } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index 4a50f12095..8291d1c0bb 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A node in the scene graph which represents an image draw. /// - internal class ImageNode : IDrawOperation + internal class ImageNode : DrawOperation { /// /// Initializes a new instance of the class. @@ -20,8 +20,8 @@ namespace Avalonia.Rendering.SceneGraph /// The source rect. /// The destination rect. public ImageNode(Matrix transform, IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) + : base(destRect, transform, null) { - Bounds = destRect.TransformToAABB(transform); Transform = transform; Source = source; Opacity = opacity; @@ -29,9 +29,6 @@ namespace Avalonia.Rendering.SceneGraph DestRect = destRect; } - /// - public Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// @@ -80,7 +77,7 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void Render(IDrawingContextImpl context) + public override void Render(IDrawingContextImpl context) { // TODO: Probably need to introduce some kind of locking mechanism in the case of // WriteableBitmap. @@ -89,6 +86,6 @@ namespace Avalonia.Rendering.SceneGraph } /// - public bool HitTest(Point p) => Bounds.Contains(p); + public override bool HitTest(Point p) => Bounds.Contains(p); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs index e39335b5b6..d3df478a63 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs @@ -28,8 +28,8 @@ namespace Avalonia.Rendering.SceneGraph Point p1, Point p2, IDictionary childScenes = null) + : base(new Rect(p1, p2), transform, pen) { - Bounds = new Rect(p1, p2).TransformToAABB(transform).Inflate(pen?.Thickness ?? 0); Transform = transform; Pen = pen?.ToImmutable(); P1 = p1; @@ -37,9 +37,6 @@ namespace Avalonia.Rendering.SceneGraph ChildScenes = childScenes; } - /// - public override Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs index c40869724f..28b8f53e26 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs @@ -19,6 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// The bounds of the mask. /// Child scenes for drawing visual brushes. public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary childScenes = null) + : base(Rect.Empty, Matrix.Identity, null) { Mask = mask?.ToImmutable(); MaskBounds = bounds; @@ -30,12 +31,10 @@ namespace Avalonia.Rendering.SceneGraph /// opacity mask pop. /// public OpacityMaskNode() + : base(Rect.Empty, Matrix.Identity, null) { } - /// - public override Rect Bounds => Rect.Empty; - /// /// Gets the mask to be pushed or null if the operation represents a pop. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 2affc454b5..1730621c55 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -30,8 +30,8 @@ namespace Avalonia.Rendering.SceneGraph Rect rect, float cornerRadius, IDictionary childScenes = null) + : base(rect, transform, pen) { - Bounds = rect.TransformToAABB(transform).Inflate(pen?.Thickness ?? 0); Transform = transform; Brush = brush?.ToImmutable(); Pen = pen?.ToImmutable(); @@ -40,9 +40,6 @@ namespace Avalonia.Rendering.SceneGraph ChildScenes = childScenes; } - /// - public override Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs index 058f3b1c22..6328d7dd14 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs @@ -28,8 +28,8 @@ namespace Avalonia.Rendering.SceneGraph Point origin, IFormattedTextImpl text, IDictionary childScenes = null) + : base(new Rect(origin, text.Size), transform, null) { - Bounds = new Rect(origin, text.Size).TransformToAABB(transform); Transform = transform; Foreground = foreground?.ToImmutable(); Origin = origin; @@ -37,9 +37,6 @@ namespace Avalonia.Rendering.SceneGraph ChildScenes = childScenes; } - /// - public override Rect Bounds { get; } - /// /// Gets the transform with which the node will be drawn. /// diff --git a/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs b/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs index 00d1ec05f3..455b63b89e 100644 --- a/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs +++ b/src/Gtk/Avalonia.Gtk3/FramebufferManager.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Platform; +using Avalonia.Threading; namespace Avalonia.Gtk3 { @@ -27,7 +28,38 @@ namespace Avalonia.Gtk3 var s = _window.ClientSize; var width = (int) s.Width; var height = (int) s.Height; - return new ImageSurfaceFramebuffer(_window, width, height); + + if (!Dispatcher.UIThread.CheckAccess() && Gtk3Platform.DisplayClassName.ToLower().Contains("x11")) + { + var x11 = LockX11Framebuffer(width, height); + if (x11 != null) + return x11; + } + + + return new ImageSurfaceFramebuffer(_window, width, height, _window.LastKnownScaleFactor); + } + + private static int X11ErrorHandler(IntPtr d, IntPtr e) + { + return 0; + } + + private static X11.XErrorHandler X11ErrorHandlerDelegate = X11ErrorHandler; + + private static IntPtr X11Display; + private ILockedFramebuffer LockX11Framebuffer(int width, int height) + { + if (!_window.GdkWindowHandle.HasValue) + return null; + if (X11Display == IntPtr.Zero) + { + X11Display = X11.XOpenDisplay(IntPtr.Zero); + if (X11Display == IntPtr.Zero) + return null; + X11.XSetErrorHandler(X11ErrorHandlerDelegate); + } + return new X11Framebuffer(X11Display, _window.GdkWindowHandle.Value, width, height, _window.LastKnownScaleFactor); } } } diff --git a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs index b36a1cda91..10405037ab 100644 --- a/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs +++ b/src/Gtk/Avalonia.Gtk3/Gtk3Platform.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -22,11 +23,15 @@ namespace Avalonia.Gtk3 internal static readonly MouseDevice Mouse = new MouseDevice(); internal static readonly KeyboardDevice Keyboard = new KeyboardDevice(); internal static IntPtr App { get; set; } + internal static string DisplayClassName; public static bool UseDeferredRendering = true; public static void Initialize() { Resolver.Resolve(); Native.GtkInit(0, IntPtr.Zero); + var disp = Native.GdkGetDefaultDisplay(); + DisplayClassName = Utf8Buffer.StringFromPtr(Native.GTypeName(Marshal.ReadIntPtr(Marshal.ReadIntPtr(disp)))); + using (var utf = new Utf8Buffer("avalonia.app." + Guid.NewGuid())) App = Native.GtkApplicationNew(utf, 0); //Mark current thread as UI thread diff --git a/src/Gtk/Avalonia.Gtk3/IDeferredRenderOperation.cs b/src/Gtk/Avalonia.Gtk3/IDeferredRenderOperation.cs index 8c1456726c..e16463a2ef 100644 --- a/src/Gtk/Avalonia.Gtk3/IDeferredRenderOperation.cs +++ b/src/Gtk/Avalonia.Gtk3/IDeferredRenderOperation.cs @@ -4,6 +4,6 @@ namespace Avalonia.Gtk3 { public interface IDeferredRenderOperation : IDisposable { - void RenderNow(); + void RenderNow(IntPtr? ctx); } } \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs index 34a95df47e..efda21c753 100644 --- a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs +++ b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs @@ -16,23 +16,23 @@ namespace Avalonia.Gtk3 { private readonly WindowBaseImpl _impl; private readonly GtkWidget _widget; - private CairoSurface _surface; + private ManagedCairoSurface _surface; private int _factor; private object _lock = new object(); - public ImageSurfaceFramebuffer(WindowBaseImpl impl, int width, int height) + public ImageSurfaceFramebuffer(WindowBaseImpl impl, int width, int height, int factor) { _impl = impl; _widget = impl.GtkWidget; - _factor = (int)(Native.GtkWidgetGetScaleFactor?.Invoke(_widget) ?? 1u); + _factor = factor; width *= _factor; height *= _factor; - _surface = Native.CairoImageSurfaceCreate(1, width, height); + _surface = new ManagedCairoSurface(width, height); Width = width; Height = height; - Address = Native.CairoImageSurfaceGetData(_surface); - RowBytes = Native.CairoImageSurfaceGetStride(_surface); - Native.CairoSurfaceFlush(_surface); + Address = _surface.Buffer; + RowBytes = _surface.Stride; + Native.CairoSurfaceFlush(_surface.Surface); } static void Draw(IntPtr context, CairoSurface surface, double factor) @@ -83,15 +83,15 @@ namespace Avalonia.Gtk3 class RenderOp : IDeferredRenderOperation { private readonly GtkWidget _widget; - private CairoSurface _surface; + private ManagedCairoSurface _surface; private readonly double _factor; private readonly int _width; private readonly int _height; - public RenderOp(GtkWidget widget, CairoSurface _surface, double factor, int width, int height) + public RenderOp(GtkWidget widget, ManagedCairoSurface surface, double factor, int width, int height) { _widget = widget; - this._surface = _surface; + _surface = surface ?? throw new ArgumentNullException(); _factor = factor; _width = width; _height = height; @@ -103,9 +103,12 @@ namespace Avalonia.Gtk3 _surface = null; } - public void RenderNow() + public void RenderNow(IntPtr? ctx) { - DrawToWidget(_widget, _surface, _width, _height, _factor); + if(ctx.HasValue) + Draw(ctx.Value, _surface.Surface, _factor); + else + DrawToWidget(_widget, _surface.Surface, _width, _height, _factor); } } @@ -116,9 +119,9 @@ namespace Avalonia.Gtk3 if (Dispatcher.UIThread.CheckAccess()) { if (_impl.CurrentCairoContext != IntPtr.Zero) - Draw(_impl.CurrentCairoContext, _surface, _factor); + Draw(_impl.CurrentCairoContext, _surface.Surface, _factor); else - DrawToWidget(_widget, _surface, Width, Height, _factor); + DrawToWidget(_widget, _surface.Surface, Width, Height, _factor); _surface.Dispose(); } else diff --git a/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs b/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs index be886ea1c7..5253be5afb 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs @@ -53,7 +53,7 @@ namespace Avalonia.Gtk3.Interop if (interval == 0) throw new ArgumentException("Don't know how to create a timer with zero or negative interval"); var timer = new Timer (); - GlibTimeout.Add(GlibPriority.FromDispatcherPriority(DispatcherPriority.Background), interval, + GlibTimeout.Add(GlibPriority.FromDispatcherPriority(DispatcherPriority.Normal), interval, () => { if (timer.Stopped) diff --git a/src/Gtk/Avalonia.Gtk3/Interop/ManagedCairoSurface.cs b/src/Gtk/Avalonia.Gtk3/Interop/ManagedCairoSurface.cs new file mode 100644 index 0000000000..2cde99afa5 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/Interop/ManagedCairoSurface.cs @@ -0,0 +1,38 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Platform; + +namespace Avalonia.Gtk3.Interop +{ + class ManagedCairoSurface : IDisposable + { + public IntPtr Buffer { get; private set; } + public CairoSurface Surface { get; private set; } + public int Stride { get; private set; } + private int _size; + private IRuntimePlatform _plat; + private IUnmanagedBlob _blob; + + public ManagedCairoSurface(int width, int height) + { + _plat = AvaloniaLocator.Current.GetService(); + Stride = width * 4; + _size = height * Stride; + _blob = _plat.AllocBlob(_size * 2); + Buffer = _blob.Address; + Surface = Native.CairoImageSurfaceCreateForData(Buffer, 1, width, height, Stride); + } + + public void Dispose() + { + + if (Buffer != IntPtr.Zero) + { + Surface.Dispose(); + _blob.Dispose(); + Buffer = IntPtr.Zero; + } + } + + } +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs index a6a08c3614..15b3a11fbb 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/Native.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/Native.cs @@ -160,6 +160,9 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Cairo)] public delegate CairoSurface cairo_image_surface_create(int format, int width, int height); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Cairo)] + public delegate CairoSurface cairo_image_surface_create_for_data(IntPtr data, int format, int width, int height, int stride); [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Cairo)] public delegate IntPtr cairo_image_surface_get_data(CairoSurface surface); @@ -178,7 +181,7 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Cairo)] public delegate void cairo_surface_destroy(IntPtr surface); - + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Cairo)] public delegate void cairo_set_source_surface(IntPtr cr, CairoSurface surface, double x, double y); @@ -236,17 +239,17 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] public delegate GdkWindowState gdk_window_get_state(IntPtr window); - [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] - public delegate void gdk_window_iconify(IntPtr window); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_iconify(GtkWindow window); - [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] - public delegate void gdk_window_deiconify(IntPtr window); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_deiconify(GtkWindow window); - [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] - public delegate void gdk_window_maximize(IntPtr window); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_maximize(GtkWindow window); - [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gdk)] - public delegate void gdk_window_unmaximize(IntPtr window); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gtk)] + public delegate void gtk_window_unmaximize(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); @@ -315,6 +318,9 @@ namespace Avalonia.Gtk3.Interop [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] public delegate void g_object_ref(GObject instance); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] + public delegate IntPtr g_type_name(IntPtr instance); + [UnmanagedFunctionPointer(CallingConvention.Cdecl), GtkImport(GtkDll.Gobject)] public delegate ulong g_signal_connect_object(GObject instance, Utf8Buffer signal, IntPtr handler, IntPtr userData, int flags); @@ -407,6 +413,7 @@ namespace Avalonia.Gtk3.Interop public static D.gtk_dialog_add_button GtkDialogAddButton; public static D.g_object_unref GObjectUnref; public static D.g_object_ref GObjectRef; + public static D.g_type_name GTypeName; public static D.g_signal_connect_object GSignalConnectObject; public static D.g_signal_handler_disconnect GSignalHandlerDisconnect; public static D.g_timeout_add GTimeoutAdd; @@ -437,10 +444,10 @@ namespace Avalonia.Gtk3.Interop public static D.gdk_window_get_origin GdkWindowGetOrigin; public static D.gdk_window_get_pointer GdkWindowGetPointer; public static D.gdk_window_get_state GdkWindowGetState; - public static D.gdk_window_iconify GdkWindowIconify; - public static D.gdk_window_deiconify GdkWindowDeiconify; - public static D.gdk_window_maximize GdkWindowMaximize; - public static D.gdk_window_unmaximize GdkWindowUnmaximize; + public static D.gtk_window_iconify GtkWindowIconify; + public static D.gtk_window_deiconify GtkWindowDeiconify; + public static D.gtk_window_maximize GtkWindowMaximize; + public static D.gtk_window_unmaximize GtkWindowUnmaximize; 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; @@ -459,6 +466,7 @@ namespace Avalonia.Gtk3.Interop public static D.gdk_cairo_create GdkCairoCreate; public static D.cairo_image_surface_create CairoImageSurfaceCreate; + public static D.cairo_image_surface_create_for_data CairoImageSurfaceCreateForData; public static D.cairo_image_surface_get_data CairoImageSurfaceGetData; public static D.cairo_image_surface_get_stride CairoImageSurfaceGetStride; public static D.cairo_surface_mark_dirty CairoSurfaceMarkDirty; diff --git a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs index a3b5d57fdc..e14ed77877 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowBaseImpl.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Text; +using System.Threading; using Avalonia.Controls; using Avalonia.Gtk3.Interop; using Avalonia.Input; @@ -29,7 +30,8 @@ namespace Avalonia.Gtk3 private GCHandle _gcHandle; private object _lock = new object(); private IDeferredRenderOperation _nextRenderOperation; - + private readonly AutoResetEvent _canSetNextOperation = new AutoResetEvent(true); + internal IntPtr? GdkWindowHandle; public WindowBaseImpl(GtkWindow gtkWidget) { @@ -53,12 +55,8 @@ namespace Avalonia.Gtk3 ConnectEvent("leave-notify-event", OnLeaveNotifyEvent); Connect("destroy", OnDestroy); Native.GtkWidgetRealize(gtkWidget); + GdkWindowHandle = this.Handle.Handle; _lastSize = ClientSize; - GlibTimeout.Add(0, 16, () => - { - Invalidate(default(Rect)); - return true; - }); if (Gtk3Platform.UseDeferredRendering) { Native.GtkWidgetSetDoubleBuffered(gtkWidget, false); @@ -138,7 +136,7 @@ namespace Avalonia.Gtk3 ? RawMouseEventType.LeftButtonDown : evnt->button == 3 ? RawMouseEventType.RightButtonDown : RawMouseEventType.MiddleButtonDown, new Point(evnt->x, evnt->y), GetModifierKeys(evnt->state)); - Input?.Invoke(e); + OnInput(e); return true; } @@ -166,7 +164,7 @@ namespace Avalonia.Gtk3 _inputRoot, RawMouseEventType.Move, position, GetModifierKeys(evnt->state)); - Input(e); + OnInput(e); return true; } @@ -195,7 +193,7 @@ namespace Avalonia.Gtk3 } var e = new RawMouseWheelEventArgs(Gtk3Platform.Mouse, evnt->time, _inputRoot, new Point(evnt->x, evnt->y), delta, GetModifierKeys(evnt->state)); - Input(e); + OnInput(e); return true; } @@ -210,7 +208,7 @@ namespace Avalonia.Gtk3 evnt->time, evnt->type == GdkEventType.KeyPress ? RawKeyEventType.KeyDown : RawKeyEventType.KeyUp, Avalonia.Gtk.Common.KeyTransform.ConvertKey((GdkKey)evnt->keyval), GetModifierKeys((GdkModifierType)evnt->state)); - Input(e); + OnInput(e); return true; } @@ -218,7 +216,7 @@ namespace Avalonia.Gtk3 { var evnt = (GdkEventCrossing*) pev; var position = new Point(evnt->x, evnt->y); - Input(new RawMouseEventArgs(Gtk3Platform.Mouse, + OnInput(new RawMouseEventArgs(Gtk3Platform.Mouse, evnt->time, _inputRoot, RawMouseEventType.Move, @@ -228,7 +226,7 @@ namespace Avalonia.Gtk3 private unsafe bool OnCommit(IntPtr gtkwidget, IntPtr utf8string, IntPtr userdata) { - Input(new RawTextInputEventArgs(Gtk3Platform.Keyboard, _lastKbdEvent, Utf8Buffer.StringFromPtr(utf8string))); + OnInput(new RawTextInputEventArgs(Gtk3Platform.Keyboard, _lastKbdEvent, Utf8Buffer.StringFromPtr(utf8string))); return true; } @@ -260,11 +258,19 @@ namespace Avalonia.Gtk3 public void SetNextRenderOperation(IDeferredRenderOperation op) { - lock (_lock) + while (true) { - _nextRenderOperation?.Dispose(); - _nextRenderOperation = op; + lock (_lock) + { + if (_nextRenderOperation == null) + { + _nextRenderOperation = op; + return; + } + } + _canSetNextOperation.WaitOne(); } + } private void OnRenderTick() @@ -277,10 +283,11 @@ namespace Avalonia.Gtk3 op = _nextRenderOperation; _nextRenderOperation = null; } + _canSetNextOperation.Set(); } if (op != null) { - op?.RenderNow(); + op?.RenderNow(null); op?.Dispose(); } } @@ -311,7 +318,7 @@ namespace Avalonia.Gtk3 public IMouseDevice MouseDevice => Gtk3Platform.Mouse; - public double Scaling => (double) 1 / (Native.GtkWidgetGetScaleFactor?.Invoke(GtkWidget) ?? 1); + public double Scaling => LastKnownScaleFactor = (int) (Native.GtkWidgetGetScaleFactor?.Invoke(GtkWidget) ?? 1); public IPlatformHandle Handle => this; @@ -338,6 +345,11 @@ namespace Avalonia.Gtk3 public void SetInputRoot(IInputRoot inputRoot) => _inputRoot = inputRoot; + void OnInput(RawInputEventArgs args) + { + Dispatcher.UIThread.InvokeAsync(() => Input?.Invoke(args), DispatcherPriority.Input); + } + public Point PointToClient(Point point) { int x, y; @@ -387,6 +399,7 @@ namespace Avalonia.Gtk3 public Size ClientSize { get; private set; } + public int LastKnownScaleFactor { get; private set; } public void Resize(Size value) { diff --git a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs index a4bc45c0a1..c586661a7a 100644 --- a/src/Gtk/Avalonia.Gtk3/WindowImpl.cs +++ b/src/Gtk/Avalonia.Gtk3/WindowImpl.cs @@ -32,15 +32,14 @@ namespace Avalonia.Gtk3 } set { - var w = Native.GtkWidgetGetWindow(GtkWidget); if (value == WindowState.Minimized) - Native.GdkWindowIconify(w); + Native.GtkWindowIconify(GtkWidget); else if (value == WindowState.Maximized) - Native.GdkWindowMaximize(w); + Native.GtkWindowMaximize(GtkWidget); else { - Native.GdkWindowUnmaximize(w); - Native.GdkWindowDeiconify(w); + Native.GtkWindowUnmaximize(GtkWidget); + Native.GtkWindowDeiconify(GtkWidget); } } } diff --git a/src/Gtk/Avalonia.Gtk3/X11.cs b/src/Gtk/Avalonia.Gtk3/X11.cs new file mode 100644 index 0000000000..6708ece17b --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/X11.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.Gtk3 +{ + class X11 + { + [DllImport("libX11.so.6")] + public static extern IntPtr XOpenDisplay(IntPtr name); + + [DllImport("libX11.so.6")] + public static extern IntPtr XFreeGC(IntPtr display, IntPtr gc); + + [DllImport("libX11.so.6")] + public static extern IntPtr XCreateGC(IntPtr display, IntPtr drawable, ulong valuemask, IntPtr values); + + [DllImport("libX11.so.6")] + public static extern int XInitImage(ref XImage image); + + [DllImport("libX11.so.6")] + public static extern int XDestroyImage(ref XImage image); + + [DllImport("libX11.so.6")] + public static extern IntPtr XSetErrorHandler(XErrorHandler handler); + + public delegate int XErrorHandler(IntPtr display, IntPtr error); + + [DllImport("libX11.so.6")] + public static extern int XPutImage(IntPtr display, IntPtr drawable, IntPtr gc, ref XImage image, + int srcx, int srcy, int destx, int desty, uint width, uint height); + + + public unsafe struct XImage + { + public int width, height; /* size of image */ + public int xoffset; /* number of pixels offset in X direction */ + public int format; /* XYBitmap, XYPixmap, ZPixmap */ + public IntPtr data; /* pointer to image data */ + public int byte_order; /* data byte order, LSBFirst, MSBFirst */ + public int bitmap_unit; /* quant. of scanline 8, 16, 32 */ + public int bitmap_bit_order; /* LSBFirst, MSBFirst */ + public int bitmap_pad; /* 8, 16, 32 either XY or ZPixmap */ + public int depth; /* depth of image */ + public int bytes_per_line; /* accelerator to next scanline */ + public int bits_per_pixel; /* bits per pixel (ZPixmap) */ + public ulong red_mask; /* bits in z arrangement */ + public ulong green_mask; + public ulong blue_mask; + private fixed byte funcs[128]; + } + + + } +} \ No newline at end of file diff --git a/src/Gtk/Avalonia.Gtk3/X11Framebuffer.cs b/src/Gtk/Avalonia.Gtk3/X11Framebuffer.cs new file mode 100644 index 0000000000..2bf08bddf1 --- /dev/null +++ b/src/Gtk/Avalonia.Gtk3/X11Framebuffer.cs @@ -0,0 +1,55 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Platform; + +namespace Avalonia.Gtk3 +{ + class X11Framebuffer : ILockedFramebuffer + { + private readonly IntPtr _display; + private readonly IntPtr _xid; + private IUnmanagedBlob _blob; + + public X11Framebuffer(IntPtr display, IntPtr xid, int width, int height, int factor) + { + _display = display; + _xid = xid; + Width = width*factor; + Height = height*factor; + RowBytes = Width * 4; + Dpi = new Vector(96, 96) * factor; + Format = PixelFormat.Bgra8888; + _blob = AvaloniaLocator.Current.GetService().AllocBlob(RowBytes * Height); + Address = _blob.Address; + } + + public void Dispose() + { + var image = new X11.XImage(); + int bitsPerPixel = 32; + image.width = Width; + image.height = Height; + image.format = 2; //ZPixmap; + image.data = Address; + image.byte_order = 0;// LSBFirst; + image.bitmap_unit = bitsPerPixel; + image.bitmap_bit_order = 0;// LSBFirst; + image.bitmap_pad = bitsPerPixel; + image.depth = 24; + image.bytes_per_line = RowBytes - Width * 4; + image.bits_per_pixel = bitsPerPixel; + X11.XInitImage(ref image); + var gc = X11.XCreateGC(_display, _xid, 0, IntPtr.Zero); + X11.XPutImage(_display, _xid, gc, ref image, 0, 0, 0, 0, (uint) Width, (uint) Height); + X11.XFreeGC(_display, gc); + _blob.Dispose(); + } + + public IntPtr Address { get; } + public int Width { get; } + public int Height { get; } + public int RowBytes { get; } + public Vector Dpi { get; } + public PixelFormat Format { get; } + } +} \ No newline at end of file diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 092910a08f..b777736f06 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -2,8 +2,11 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections.Generic; using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using Avalonia.Platform; @@ -11,20 +14,145 @@ namespace Avalonia.Shared.PlatformSupport { internal partial class StandardRuntimePlatform : IRuntimePlatform { - -#if NETCOREAPP2_0 - public void PostThreadPoolItem(Action cb) => ThreadPool.QueueUserWorkItem(_ => cb(), null); -#else - public Assembly[] GetLoadedAssemblies() => AppDomain.CurrentDomain.GetAssemblies(); public void PostThreadPoolItem(Action cb) => ThreadPool.UnsafeQueueUserWorkItem(_ => cb(), null); -#endif + public Assembly[] GetLoadedAssemblies() => AppDomain.CurrentDomain.GetAssemblies(); public IDisposable StartSystemTimer(TimeSpan interval, Action tick) { return new Timer(_ => tick(), null, interval, interval); } + public string GetStackTrace() => Environment.StackTrace; + + public IUnmanagedBlob AllocBlob(int size) => new UnmanagedBlob(this, size); + + class UnmanagedBlob : IUnmanagedBlob + { + private readonly StandardRuntimePlatform _plat; +#if DEBUG + private static readonly List Backtraces = new List(); + private static Thread GCThread; + private readonly string _backtrace; - public string GetStackTrace() => Environment.StackTrace; + class GCThreadDetector + { + ~GCThreadDetector() + { + GCThread = Thread.CurrentThread; + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void Spawn() => new GCThreadDetector(); + + static UnmanagedBlob() + { + Spawn(); + GC.WaitForPendingFinalizers(); + } + +#endif + + public UnmanagedBlob(StandardRuntimePlatform plat, int size) + { + _plat = plat; + Address = plat.Alloc(size); + GC.AddMemoryPressure(size); + Size = size; +#if DEBUG + _backtrace = Environment.StackTrace; + Backtraces.Add(_backtrace); +#endif + } + + void DoDispose() + { + if (!IsDisposed) + { +#if DEBUG + Backtraces.Remove(_backtrace); +#endif + _plat.Free(Address, Size); + GC.RemoveMemoryPressure(Size); + IsDisposed = true; + Address = IntPtr.Zero; + Size = 0; + } + } + + public void Dispose() + { +#if DEBUG + if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) + { + Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " + + Environment.StackTrace + + "\n\nBlob created by " + _backtrace); + } +#endif + DoDispose(); + GC.SuppressFinalize(this); + } + + ~UnmanagedBlob() + { +#if DEBUG + Console.Error.WriteLine("Undisposed native blob created by " + _backtrace); +#endif + DoDispose(); + } + + public IntPtr Address { get; private set; } + public int Size { get; private set; } + public bool IsDisposed { get; private set; } + } + + + +#if FULLDOTNET || DOTNETCORE + [DllImport("libc", SetLastError = true)] + private static extern IntPtr mmap(IntPtr addr, IntPtr length, int prot, int flags, int fd, IntPtr offset); + [DllImport("libc", SetLastError = true)] + private static extern int munmap(IntPtr addr, IntPtr length); + [DllImport("libc", SetLastError = true)] + private static extern long sysconf(int name); + + private bool? _useMmap; + private bool UseMmap + => _useMmap ?? ((_useMmap = GetRuntimeInfo().OperatingSystem == OperatingSystemType.Linux)).Value; + + IntPtr Alloc(int size) + { + if (UseMmap) + { + var rv = mmap(IntPtr.Zero, new IntPtr(size), 3, 0x22, -1, IntPtr.Zero); + if (rv.ToInt64() == -1 || (ulong) rv.ToInt64() == 0xffffffff) + { + var errno = Marshal.GetLastWin32Error(); + throw new Exception("Unable to allocate memory: " + errno); + } + return rv; + } + else + return Marshal.AllocHGlobal(size); + } + + void Free(IntPtr ptr, int len) + { + if (UseMmap) + { + if (munmap(ptr, new IntPtr(len)) == -1) + { + var errno = Marshal.GetLastWin32Error(); + throw new Exception("Unable to free memory: " + errno); + } + } + else + Marshal.FreeHGlobal(ptr); + } +#else + IntPtr Alloc(int size) => Marshal.AllocHGlobal(size); + void Free(IntPtr ptr, int len) => Marshal.FreeHGlobal(ptr); +#endif } } \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/BitmapImpl.cs b/src/Skia/Avalonia.Skia/BitmapImpl.cs index ce3efded11..00ab770e01 100644 --- a/src/Skia/Avalonia.Skia/BitmapImpl.cs +++ b/src/Skia/Avalonia.Skia/BitmapImpl.cs @@ -20,16 +20,35 @@ namespace Avalonia.Skia _dpi = new Vector(96, 96); } + static void ReleaseProc(IntPtr address, object ctx) + { + ((IUnmanagedBlob) ctx).Dispose(); + } + + private static readonly SKBitmapReleaseDelegate ReleaseDelegate = ReleaseProc; + public BitmapImpl(int width, int height, Vector dpi, PixelFormat? fmt = null) { PixelHeight = height; PixelWidth = width; _dpi = dpi; var colorType = fmt?.ToSkColorType() ?? SKImageInfo.PlatformColorType; - var runtime = AvaloniaLocator.Current?.GetService()?.GetRuntimeInfo(); + var runtimePlatform = AvaloniaLocator.Current?.GetService(); + var runtime = runtimePlatform?.GetRuntimeInfo(); if (runtime?.IsDesktop == true && runtime?.OperatingSystem == OperatingSystemType.Linux) colorType = SKColorType.Bgra8888; - Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); + + if (runtimePlatform != null) + { + Bitmap = new SKBitmap(); + var nfo = new SKImageInfo(width, height, colorType, SKAlphaType.Premul); + var plat = AvaloniaLocator.Current.GetService(); + var blob = plat.AllocBlob(nfo.BytesSize); + Bitmap.InstallPixels(nfo, blob.Address, nfo.RowBytes, null, ReleaseDelegate, blob); + + } + else + Bitmap = new SKBitmap(width, height, colorType, SKAlphaType.Premul); Bitmap.Erase(SKColor.Empty); } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 4feb910deb..e549f06bd5 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -45,7 +45,7 @@ namespace Avalonia.Skia var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); using (var paint = new SKPaint() - { Color = new SKColor(255, 255, 255, (byte)(255 * opacity)) }) + { Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity)) }) { Canvas.DrawBitmap(impl.Bitmap, s, d, paint); } @@ -112,6 +112,7 @@ namespace Avalonia.Skia public readonly SKPaint Paint; private IDisposable _disposable1; + private IDisposable _disposable2; public IDisposable ApplyTo(SKPaint paint) { @@ -127,6 +128,8 @@ namespace Avalonia.Skia { if (_disposable1 == null) _disposable1 = disposable; + else if (_disposable2 == null) + _disposable2 = disposable; else throw new InvalidOperationException(); } @@ -135,12 +138,14 @@ namespace Avalonia.Skia { Paint = paint; _disposable1 = null; + _disposable2 = null; } public void Dispose() { Paint?.Dispose(); _disposable1?.Dispose(); + _disposable2?.Dispose(); } } @@ -221,8 +226,8 @@ namespace Avalonia.Skia _visualBrushRenderer.RenderVisualBrush(ctx, visualBrush); } - rv.AddDisposable(tileBrushImage); tileBrushImage = intermediate; + rv.AddDisposable(tileBrushImage); } } else diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index fd8364c03b..90409bb208 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -146,6 +146,8 @@ namespace Avalonia.Direct2D1 } if (s is IExternalDirect2DRenderTargetSurface external) return new ExternalRenderTarget(external, s_dwfactory); + if (s is IFramebufferPlatformSurface fb) + return new FramebufferShimRenderTarget(fb, s_imagingFactory, s_d2D1Factory, s_dwfactory); } throw new NotSupportedException("Don't know how to create a Direct2D1 renderer from any of provided surfaces"); } diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs new file mode 100644 index 0000000000..d465af5322 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Direct2D1.Media; +using Avalonia.Direct2D1.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Win32.Interop; +using SharpDX.Direct2D1; +using SharpDX.WIC; +using PixelFormat = Avalonia.Platform.PixelFormat; + +namespace Avalonia.Direct2D1 +{ + class FramebufferShimRenderTarget : IRenderTarget + { + private readonly IFramebufferPlatformSurface _surface; + private readonly ImagingFactory _imagingFactory; + private readonly Factory _d2DFactory; + private readonly SharpDX.DirectWrite.Factory _dwriteFactory; + + public FramebufferShimRenderTarget(IFramebufferPlatformSurface surface, + ImagingFactory imagingFactory, Factory d2dFactory, SharpDX.DirectWrite.Factory dwriteFactory) + { + _surface = surface; + _imagingFactory = imagingFactory; + _d2DFactory = d2dFactory; + _dwriteFactory = dwriteFactory; + } + + public void Dispose() + { + + } + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + var locked = _surface.Lock(); + if (locked.Format == PixelFormat.Rgb565) + { + locked.Dispose(); + throw new ArgumentException("Unsupported pixel format: " + locked.Format); + } + + return new FramebufferShim(locked, _imagingFactory, _d2DFactory, _dwriteFactory) + .CreateDrawingContext(visualBrushRenderer); + } + + class FramebufferShim : RenderTargetBitmapImpl + { + private readonly ILockedFramebuffer _target; + + public FramebufferShim(ILockedFramebuffer target, + ImagingFactory imagingFactory, Factory d2dFactory, SharpDX.DirectWrite.Factory dwriteFactory + ) : base(imagingFactory, d2dFactory, dwriteFactory, + target.Width, target.Height, target.Dpi.X, target.Dpi.Y, target.Format) + { + _target = target; + } + + public override IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + return base.CreateDrawingContext(visualBrushRenderer, () => + { + using (var l = WicImpl.Lock(BitmapLockFlags.Read)) + { + for (var y = 0; y < _target.Height; y++) + { + UnmanagedMethods.CopyMemory( + _target.Address + _target.RowBytes * y, + l.Data.DataPointer + l.Stride * y, + (uint) Math.Min(l.Stride, _target.RowBytes)); + } + } + Dispose(); + _target.Dispose(); + + }); + } + } + + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs index 33736b02cb..0d6ed9f39f 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs @@ -22,8 +22,9 @@ namespace Avalonia.Direct2D1.Media int width, int height, double dpiX, - double dpiY) - : base(imagingFactory, width, height) + double dpiY, + Platform.PixelFormat? pixelFormat = null) + : base(imagingFactory, width, height, pixelFormat) { var props = new RenderTargetProperties { @@ -45,9 +46,13 @@ namespace Avalonia.Direct2D1.Media base.Dispose(); } - public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + public virtual IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + => CreateDrawingContext(visualBrushRenderer, null); + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer, Action finishedCallback) { - return new DrawingContextImpl(visualBrushRenderer, _target, _dwriteFactory); + return new DrawingContextImpl(visualBrushRenderer, _target, _dwriteFactory, + finishedCallback: finishedCallback); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index e817dd4812..bcce2496cd 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -74,6 +74,7 @@ namespace Avalonia.Direct2D1.Media public WicBitmapImpl(ImagingFactory factory, Platform.PixelFormat format, IntPtr data, int width, int height, int stride) { WicImpl = new Bitmap(factory, width, height, format.ToWic(), BitmapCreateCacheOption.CacheOnDemand); + _factory = factory; PixelFormat = format; using (var l = WicImpl.Lock(BitmapLockFlags.Write)) { diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index 05339c43b0..e9cb2bf450 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -187,7 +187,7 @@ namespace Avalonia.Base.UnitTests source.OnNext(45); - Assert.Equal(null, target.Foo); + Assert.Null(target.Foo); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index 01b550fb3b..9b4be59647 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -66,7 +66,7 @@ namespace Avalonia.Controls.UnitTests.Generators target.Dematerialize(1, 1); Assert.Equal(containers[0].ContainerControl, target.ContainerFromIndex(0)); - Assert.Equal(null, target.ContainerFromIndex(1)); + Assert.Null(target.ContainerFromIndex(1)); Assert.Equal(containers[2].ContainerControl, target.ContainerFromIndex(2)); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index e32c703409..a524ca3e89 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -107,7 +107,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = child; target.Content = null; - Assert.Equal(null, child.GetLogicalParent()); + Assert.Null(child.GetLogicalParent()); Assert.Empty(target.GetLogicalChildren()); } @@ -120,7 +120,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = child; target.Content = null; - Assert.Equal(null, child.GetVisualParent()); + Assert.Null(child.GetVisualParent()); Assert.Empty(target.GetVisualChildren()); } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index 589b1d67d2..032928d673 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -191,7 +191,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = "bar"; target.UpdateChild(); - Assert.Equal(null, foo.Parent); + Assert.Null(foo.Parent); logicalChildren = target.GetLogicalChildren(); diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 7ee9fbbf52..a60074fa43 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -171,7 +171,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectedItem = new Item(); - Assert.Equal(null, target.SelectedItem); + Assert.Null(target.SelectedItem); Assert.Equal(-1, target.SelectedIndex); } @@ -278,7 +278,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Items = null; - Assert.Equal(null, target.SelectedItem); + Assert.Null(target.SelectedItem); Assert.Equal(-1, target.SelectedIndex); } @@ -305,7 +305,7 @@ namespace Avalonia.Controls.UnitTests.Primitives items.RemoveAt(1); - Assert.Equal(null, target.SelectedItem); + Assert.Null(target.SelectedItem); Assert.Equal(-1, target.SelectedIndex); } @@ -334,7 +334,7 @@ namespace Avalonia.Controls.UnitTests.Primitives items.Clear(); - Assert.Equal(null, target.SelectedItem); + Assert.Null(target.SelectedItem); Assert.Equal(-1, target.SelectedIndex); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index d8600f472d..642f594e4d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -76,7 +76,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.SelectedItems = new AvaloniaList(); Assert.Equal(-1, target.SelectedIndex); - Assert.Equal(null, target.SelectedItem); + Assert.Null(target.SelectedItem); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs index f75c126ff1..56412d732b 100644 --- a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs @@ -70,7 +70,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); - Assert.Equal(new Size(120, 130), target.Bounds.Size); + Assert.Equal(new Size(120, 120), target.Bounds.Size); Assert.Equal(new Rect(0, 0, 120, 20), target.Children[0].Bounds); Assert.Equal(new Rect(0, 30, 120, 30), target.Children[1].Bounds); Assert.Equal(new Rect(0, 70, 120, 50), target.Children[2].Bounds); @@ -94,7 +94,7 @@ namespace Avalonia.Controls.UnitTests target.Measure(Size.Infinity); target.Arrange(new Rect(target.DesiredSize)); - Assert.Equal(new Size(130, 120), target.Bounds.Size); + Assert.Equal(new Size(120, 120), target.Bounds.Size); Assert.Equal(new Rect(0, 0, 20, 120), target.Children[0].Bounds); Assert.Equal(new Rect(30, 0, 30, 120), target.Children[1].Bounds); Assert.Equal(new Rect(70, 0, 50, 120), target.Children[2].Bounds); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_RelativeSource.cs index e912770470..c46fb6fce2 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_RelativeSource.cs @@ -117,7 +117,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data }; target.Bind(TextBox.TextProperty, binding); - Assert.Equal(null, target.Text); + Assert.Null(target.Text); } [Fact] diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs index 49a88e8fae..aa1e56f2a5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Templates/MemberSelectorTests.cs @@ -63,7 +63,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Templates { var selector = new MemberSelector() { MemberName = "StringValue" }; - Assert.Equal(null, selector.Select(null)); + Assert.Null(selector.Select(null)); } [Fact] @@ -73,7 +73,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Templates var data = new Item() { StringValue = "Value1" }; - Assert.Same(null, selector.Select(data)); + Assert.Null(selector.Select(data)); } [Fact] diff --git a/tests/Avalonia.RenderTests/Media/BitmapTests.cs b/tests/Avalonia.RenderTests/Media/BitmapTests.cs index f01f78ae94..a7cd06a894 100644 --- a/tests/Avalonia.RenderTests/Media/BitmapTests.cs +++ b/tests/Avalonia.RenderTests/Media/BitmapTests.cs @@ -64,13 +64,13 @@ namespace Avalonia.Direct2D1.RenderTests.Media public void Deallocate() => Marshal.FreeHGlobal(Address); } - -#if AVALONIA_SKIA + [Theory] -#else - [Theory(Skip = "Framebuffer not supported")] + [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888), +#if SKIA + InlineData(PixelFormat.Rgb565) #endif - [InlineData(PixelFormat.Rgba8888), InlineData(PixelFormat.Bgra8888), InlineData(PixelFormat.Rgb565)] + ] public void FramebufferRenderResultsShouldBeUsableAsBitmap(PixelFormat fmt) { var testName = nameof(FramebufferRenderResultsShouldBeUsableAsBitmap) + "_" + fmt; @@ -84,6 +84,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media ctx.FillRectangle(Brushes.Chartreuse, new Rect(0, 0, 20, 100)); ctx.FillRectangle(Brushes.Crimson, new Rect(20, 0, 20, 100)); ctx.FillRectangle(Brushes.Gold, new Rect(40, 0, 20, 100)); + ctx.PopOpacity(); } var bmp = new Bitmap(fmt, fb.Address, fb.Width, fb.Height, fb.RowBytes); diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs new file mode 100644 index 0000000000..76fe103c1b --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Xunit; + +namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph +{ + public class DrawOperationTests + { + [Fact] + public void Empty_Bounds_Remain_Empty() + { + var target = new TestDrawOperation(Rect.Empty, Matrix.Identity, null); + + Assert.Equal(Rect.Empty, target.Bounds); + } + + [Theory] + [InlineData(10, 10, 10, 10, 1, 1, 1, 9, 9, 12, 12)] + [InlineData(10, 10, 10, 10, 1, 1, 2, 9, 9, 12, 12)] + [InlineData(10, 10, 10, 10, 1.5, 1.5, 1, 14, 14, 17, 17)] + public void Rectangle_Bounds_Are_Snapped_To_Pixels( + double x, + double y, + double width, + double height, + double scaleX, + double scaleY, + double? penThickness, + double expectedX, + double expectedY, + double expectedWidth, + double expectedHeight) + { + var target = new TestDrawOperation( + new Rect(x, y, width, height), + Matrix.CreateScale(scaleX, scaleY), + penThickness.HasValue ? new Pen(Brushes.Black, penThickness.Value) : null); + Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), target.Bounds); + } + + private class TestDrawOperation : DrawOperation + { + public TestDrawOperation(Rect bounds, Matrix transform, Pen pen) + :base(bounds, transform, pen) + { + } + + public override bool HitTest(Point p) => false; + + public override void Render(IDrawingContextImpl context) { } + } + } +} diff --git a/tests/TestFiles/Direct2D1/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Bgra8888.expected.png b/tests/TestFiles/Direct2D1/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Bgra8888.expected.png new file mode 100644 index 0000000000..19686464c5 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Bgra8888.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgb565.expected.png b/tests/TestFiles/Direct2D1/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgb565.expected.png new file mode 100644 index 0000000000..f3d20008a1 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgb565.expected.png differ diff --git a/tests/TestFiles/Direct2D1/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgba8888.expected.png b/tests/TestFiles/Direct2D1/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgba8888.expected.png new file mode 100644 index 0000000000..19686464c5 Binary files /dev/null and b/tests/TestFiles/Direct2D1/Media/Bitmap/FramebufferRenderResultsShouldBeUsableAsBitmap_Rgba8888.expected.png differ