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/Assets/test_icon.ico b/samples/ControlCatalog/Assets/test_icon.ico index 9d1074ad13..da8d49ff9b 100644 Binary files a/samples/ControlCatalog/Assets/test_icon.ico and b/samples/ControlCatalog/Assets/test_icon.ico differ diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 340905a7f7..11ff531514 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -69,6 +69,9 @@ Designer + + Designer + Designer @@ -131,6 +134,9 @@ MenuPage.xaml + + ProgressBarPage.xaml + RadioButtonPage.xaml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 6c432a90ac..0940316ce9 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -16,6 +16,7 @@ + diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml new file mode 100644 index 0000000000..bf40e68630 --- /dev/null +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml @@ -0,0 +1,24 @@ + + + ProgressBar + A progress bar control + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml.cs b/samples/ControlCatalog/Pages/ProgressBarPage.xaml.cs new file mode 100644 index 0000000000..56792b3908 --- /dev/null +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml.cs @@ -0,0 +1,18 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class ProgressBarPage : UserControl + { + public ProgressBarPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} 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/samples/interop/Direct3DInteropSample/MainWindow.cs b/samples/interop/Direct3DInteropSample/MainWindow.cs index ad40e81895..ffa0de0a36 100644 --- a/samples/interop/Direct3DInteropSample/MainWindow.cs +++ b/samples/interop/Direct3DInteropSample/MainWindow.cs @@ -253,8 +253,9 @@ namespace Direct3DInteropSample public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { - return new DrawingContextImpl(visualBrushRenderer, _window._d2dRenderTarget, - AvaloniaLocator.Current.GetService()); + return new DrawingContextImpl(visualBrushRenderer, null, _window._d2dRenderTarget, + AvaloniaLocator.Current.GetService(), + AvaloniaLocator.Current.GetService()); } } diff --git a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs index 2e5b2902f4..77dfc60b83 100644 --- a/src/Android/Avalonia.Android/AndroidThreadingInterface.cs +++ b/src/Android/Avalonia.Android/AndroidThreadingInterface.cs @@ -30,7 +30,7 @@ namespace Avalonia.Android return; } - public IDisposable StartTimer(TimeSpan interval, Action tick) + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { if (interval.TotalMilliseconds < 10) interval = TimeSpan.FromMilliseconds(10); diff --git a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs index 68f9e2c631..9f5417ca95 100644 --- a/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformThreadingInterface.cs @@ -17,10 +17,11 @@ namespace Avalonia.Platform /// /// Starts a timer. /// + /// /// The interval. /// The action to call on each tick. /// An used to stop the timer. - IDisposable StartTimer(TimeSpan interval, Action tick); + IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick); void Signal(DispatcherPriority priority); 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.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index a60b663bed..a4b1aaafbc 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -84,6 +84,19 @@ namespace Avalonia.Threading _jobRunner?.Post(action, priority); } + /// + /// This is needed for platform backends that don't have internal priority system (e. g. win32) + /// To ensure that there are no jobs with higher priority + /// + /// + internal void EnsurePriority(DispatcherPriority currentPriority) + { + if (currentPriority == DispatcherPriority.MaxValue) + return; + currentPriority += 1; + _jobRunner.RunJobs(currentPriority); + } + /// /// Allows unit tests to change the platform threading interface. /// diff --git a/src/Avalonia.Base/Threading/DispatcherTimer.cs b/src/Avalonia.Base/Threading/DispatcherTimer.cs index 972fdb2049..4a8a9d673f 100644 --- a/src/Avalonia.Base/Threading/DispatcherTimer.cs +++ b/src/Avalonia.Base/Threading/DispatcherTimer.cs @@ -17,13 +17,11 @@ namespace Avalonia.Threading private readonly DispatcherPriority _priority; private TimeSpan _interval; - - private readonly Action _raiseTickAction; - + /// /// Initializes a new instance of the class. /// - public DispatcherTimer() : this(DispatcherPriority.Normal) + public DispatcherTimer() : this(DispatcherPriority.Background) { } @@ -34,7 +32,6 @@ namespace Avalonia.Threading public DispatcherTimer(DispatcherPriority priority) { _priority = priority; - _raiseTickAction = RaiseTick; } /// @@ -187,7 +184,7 @@ namespace Avalonia.Threading throw new Exception("Could not start timer: IPlatformThreadingInterface is not registered."); } - _timer = threading.StartTimer(Interval, InternalTick); + _timer = threading.StartTimer(_priority, Interval, InternalTick); } } @@ -210,14 +207,7 @@ namespace Avalonia.Threading /// private void InternalTick() { - Dispatcher.UIThread.InvokeAsync(_raiseTickAction, _priority); - } - - /// - /// Raises the event. - /// - private void RaiseTick() - { + Dispatcher.UIThread.EnsurePriority(_priority); Tick?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 926c240e57..dadd3b910b 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -307,6 +307,21 @@ namespace Avalonia.Controls () => IsSubMenuOpen = true, TimeSpan.FromMilliseconds(400)); } + else + { + var parentItem = Parent as MenuItem; + if (parentItem != null) + { + foreach (var sibling in parentItem.Items + .OfType() + .Where(x => x != this && x.IsSubMenuOpen)) + { + sibling.CloseSubmenus(); + sibling.IsSubMenuOpen = false; + sibling.IsSelected = false; + } + } + } } /// diff --git a/src/Avalonia.Controls/Orientation.cs b/src/Avalonia.Controls/Orientation.cs new file mode 100644 index 0000000000..fe998c024a --- /dev/null +++ b/src/Avalonia.Controls/Orientation.cs @@ -0,0 +1,21 @@ +// 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. + +namespace Avalonia.Controls +{ + /// + /// Defines vertical or horizontal orientation. + /// + public enum Orientation + { + /// + /// Horizontal orientation. + /// + Horizontal, + + /// + /// Vertical orientation. + /// + Vertical, + } +} diff --git a/src/Avalonia.Controls/Primitives/ScrollBar.cs b/src/Avalonia.Controls/Primitives/ScrollBar.cs index 0f69e4e5bc..009e1d0ab8 100644 --- a/src/Avalonia.Controls/Primitives/ScrollBar.cs +++ b/src/Avalonia.Controls/Primitives/ScrollBar.cs @@ -30,7 +30,7 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation)); + AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); private Button _lineUpButton; private Button _lineDownButton; diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index a31a27ddfe..0ff3a78c1f 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -1,8 +1,12 @@ // 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.Reactive.Linq; + +using Avalonia.Animation; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; +using Avalonia.Layout; namespace Avalonia.Controls { @@ -11,11 +15,38 @@ namespace Avalonia.Controls /// public class ProgressBar : RangeBase { + public static readonly StyledProperty IsIndeterminateProperty = + AvaloniaProperty.Register(nameof(IsIndeterminate)); + + public static readonly StyledProperty OrientationProperty = + AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + private Border _indicator; + private IndeterminateAnimation _indeterminateAnimation; static ProgressBar() { ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); + + HorizontalAlignmentProperty.OverrideDefaultValue(HorizontalAlignment.Left); + VerticalAlignmentProperty.OverrideDefaultValue(VerticalAlignment.Top); + + IsIndeterminateProperty.Changed.AddClassHandler( + (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); }); + OrientationProperty.Changed.AddClassHandler( + (p, e) => { if (p._indicator != null) p.UpdateOrientation((Orientation)e.NewValue); }); + } + + public bool IsIndeterminate + { + get => GetValue(IsIndeterminateProperty); + set => SetValue(IsIndeterminateProperty, value); + } + + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); } /// @@ -29,21 +60,123 @@ namespace Avalonia.Controls protected override void OnTemplateApplied(TemplateAppliedEventArgs e) { _indicator = e.NameScope.Get("PART_Indicator"); + UpdateIndicator(Bounds.Size); + UpdateOrientation(Orientation); + UpdateIsIndeterminate(IsIndeterminate); } private void UpdateIndicator(Size bounds) { if (_indicator != null) { - double percent = Maximum == Minimum ? 1.0 : (Value - Minimum) / (Maximum - Minimum); - _indicator.Width = bounds.Width * percent; + if (IsIndeterminate) + { + if (Orientation == Orientation.Horizontal) + _indicator.Width = bounds.Width / 5.0; + else + _indicator.Height = bounds.Height / 5.0; + } + else + { + double percent = Maximum == Minimum ? 1.0 : (Value - Minimum) / (Maximum - Minimum); + + if (Orientation == Orientation.Horizontal) + _indicator.Width = bounds.Width * percent; + else + _indicator.Height = bounds.Height * percent; + } + } + } + + 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) + if (_indeterminateAnimation == null || _indeterminateAnimation.Disposed) + _indeterminateAnimation = IndeterminateAnimation.StartAnimation(this); + else + _indeterminateAnimation?.Dispose(); + } + private void ValueChanged(AvaloniaPropertyChangedEventArgs e) { UpdateIndicator(Bounds.Size); } + + private class IndeterminateAnimation : IDisposable + { + private WeakReference _progressBar; + private IDisposable _indeterminateBindSubscription; + private TimeSpan _startTime; + private bool _disposed; + + public bool Disposed => _disposed; + + private IndeterminateAnimation(ProgressBar progressBar) + { + _progressBar = new WeakReference(progressBar); + _startTime = Animate.Stopwatch.Elapsed; + _indeterminateBindSubscription = Animate.Timer.TakeWhile(x => (x - _startTime).TotalSeconds <= 4.0) + .Select(GetAnimationRect) + .Finally(() => _startTime = Animate.Stopwatch.Elapsed) + .Repeat() + .Subscribe(AnimationTick); + } + + public static IndeterminateAnimation StartAnimation(ProgressBar progressBar) + { + return new IndeterminateAnimation(progressBar); + } + + private Rect GetAnimationRect(TimeSpan time) + { + if (_progressBar.TryGetTarget(out var progressBar)) + { + if (progressBar.Orientation == Orientation.Horizontal) + return new Rect(-progressBar._indicator.Width - 5 + (time - _startTime).TotalSeconds / 4.0 * (progressBar.Bounds.Width + progressBar._indicator.Width + 10), 0, progressBar._indicator.Bounds.Width, progressBar._indicator.Bounds.Height); + else + return new Rect(0, progressBar.Bounds.Height + 5 - (time - _startTime).TotalSeconds / 4.0 * (progressBar.Bounds.Height + progressBar._indicator.Height + 10), progressBar._indicator.Bounds.Width, progressBar._indicator.Bounds.Height); + } + else + { + _indeterminateBindSubscription.Dispose(); + return Rect.Empty; + } + } + + private void AnimationTick(Rect rect) + { + if (_progressBar.TryGetTarget(out var progressBar)) + progressBar._indicator.Arrange(rect); + else + _indeterminateBindSubscription.Dispose(); + } + + public void Dispose() + { + _indeterminateBindSubscription?.Dispose(); + _disposed = true; + } + } } } diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index 26a755e5f1..a6fe35d668 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -6,22 +6,6 @@ using Avalonia.Input; namespace Avalonia.Controls { - /// - /// Defines vertical or horizontal orientation. - /// - public enum Orientation - { - /// - /// Vertical orientation. - /// - Vertical, - - /// - /// Horizontal orientation. - /// - Horizontal, - } - /// /// A panel which lays out its children horizontally or vertically. /// @@ -37,7 +21,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation)); + AvaloniaProperty.Register(nameof(Orientation), Orientation.Vertical); /// /// Initializes static members of the class. @@ -186,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/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index 4acff26537..82f385e16b 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -1,8 +1,6 @@ \ No newline at end of file diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 14aef8463a..3db4527bfb 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -80,12 +80,29 @@ namespace Avalonia.Platform /// The corner radius. void FillRectangle(IBrush brush, Rect rect, float cornerRadius = 0.0f); + /// + /// Creates a new that can be used as a render layer + /// for the current render target. + /// + /// The size of the layer in DIPs. + /// An + /// + /// Depending on the rendering backend used, a layer created via this method may be more + /// performant than a standard render target bitmap. In particular the Direct2D backend + /// has to do a format conversion each time a standard render target bitmap is rendered, + /// but a layer created via this method has no such overhead. + /// + IRenderTargetBitmapImpl CreateLayer(Size size); + /// /// Pushes a clip rectange. /// /// The clip rectangle. void PushClip(Rect clip); + /// + /// Pops the latest pushed clip rectangle. + /// void PopClip(); /// @@ -94,10 +111,19 @@ namespace Avalonia.Platform /// The opacity. void PushOpacity(double opacity); + /// + /// Pops the latest pushed opacity value. + /// void PopOpacity(); + /// + /// Pushes an opacity mask + /// void PushOpacityMask(IBrush mask, Rect bounds); + /// + /// Pops the latest pushed opacity mask. + /// void PopOpacityMask(); /// @@ -106,6 +132,9 @@ namespace Avalonia.Platform /// The clip geometry. void PushGeometryClip(IGeometryImpl clip); + /// + /// Pops the latest pushed geometry clip. + /// void PopGeometryClip(); } } diff --git a/src/Avalonia.Visuals/Rendering/DefaultRenderLayerFactory.cs b/src/Avalonia.Visuals/Rendering/DefaultRenderLayerFactory.cs deleted file mode 100644 index c75f948b66..0000000000 --- a/src/Avalonia.Visuals/Rendering/DefaultRenderLayerFactory.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering -{ - public class DefaultRenderLayerFactory : IRenderLayerFactory - { - private IPlatformRenderInterface _renderInterface; - - public DefaultRenderLayerFactory() - : this(AvaloniaLocator.Current.GetService()) - { - } - - public DefaultRenderLayerFactory(IPlatformRenderInterface renderInterface) - { - _renderInterface = renderInterface; - } - - public IRenderTargetBitmapImpl CreateLayer( - IVisual layerRoot, - Size size, - double dpiX, - double dpiY) - { - return _renderInterface.CreateRenderTargetBitmap( - (int)Math.Ceiling(size.Width), - (int)Math.Ceiling(size.Height), - dpiX, - dpiY); - } - } -} diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index c30fb3bdc3..5434a35464 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -26,7 +26,6 @@ namespace Avalonia.Rendering private readonly IVisual _root; private readonly ISceneBuilder _sceneBuilder; private readonly RenderLayers _layers; - private readonly IRenderLayerFactory _layerFactory; private bool _running; private Scene _scene; @@ -45,13 +44,11 @@ namespace Avalonia.Rendering /// The control to render. /// The render loop. /// The scene builder to use. Optional. - /// The layer factory to use. Optional. /// The dispatcher to use. Optional. public DeferredRenderer( IRenderRoot root, IRenderLoop renderLoop, ISceneBuilder sceneBuilder = null, - IRenderLayerFactory layerFactory = null, IDispatcher dispatcher = null) { Contract.Requires(root != null); @@ -59,8 +56,7 @@ namespace Avalonia.Rendering _dispatcher = dispatcher ?? Dispatcher.UIThread; _root = root; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _layerFactory = layerFactory ?? new DefaultRenderLayerFactory(); - _layers = new RenderLayers(_layerFactory); + _layers = new RenderLayers(); _renderLoop = renderLoop; } @@ -70,15 +66,13 @@ namespace Avalonia.Rendering /// The control to render. /// The render target. /// The scene builder to use. Optional. - /// The layer factory to use. Optional. /// /// This constructor is intended to be used for unit testing. /// public DeferredRenderer( IVisual root, IRenderTarget renderTarget, - ISceneBuilder sceneBuilder = null, - IRenderLayerFactory layerFactory = null) + ISceneBuilder sceneBuilder = null) { Contract.Requires(root != null); Contract.Requires(renderTarget != null); @@ -86,8 +80,7 @@ namespace Avalonia.Rendering _root = root; _renderTarget = renderTarget; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _layerFactory = layerFactory ?? new DefaultRenderLayerFactory(); - _layers = new RenderLayers(_layerFactory); + _layers = new RenderLayers(); } /// @@ -180,38 +173,60 @@ namespace Avalonia.Rendering bool renderOverlay = DrawDirtyRects || DrawFps; bool composite = false; + if (_renderTarget == null) + { + _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + } + if (renderOverlay) { _dirtyRectsDisplay.Tick(); } - if (scene != null && scene.Size != Size.Empty) + try { - if (scene.Generation != _lastSceneId) + if (scene != null && scene.Size != Size.Empty) { - _layers.Update(scene); - RenderToLayers(scene); + IDrawingContextImpl context = null; - if (DebugFramesPath != null) + if (scene.Generation != _lastSceneId) { - SaveDebugFrames(scene.Generation); - } + context = _renderTarget.CreateDrawingContext(this); + _layers.Update(scene, context); - _lastSceneId = scene.Generation; + RenderToLayers(scene); - composite = true; - } + if (DebugFramesPath != null) + { + SaveDebugFrames(scene.Generation); + } - if (renderOverlay) - { - RenderOverlay(scene); - RenderComposite(scene); - } - else if(composite) - { - RenderComposite(scene); + _lastSceneId = scene.Generation; + + composite = true; + } + + if (renderOverlay) + { + context = context ?? _renderTarget.CreateDrawingContext(this); + RenderOverlay(scene, context); + RenderComposite(scene, context); + } + else if (composite) + { + context = context ?? _renderTarget.CreateDrawingContext(this); + RenderComposite(scene, context); + } + + context?.Dispose(); } } + catch (RenderTargetCorruptedException ex) + { + Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); + _renderTarget?.Dispose(); + _renderTarget = null; + } } private void Render(IDrawingContextImpl context, VisualNode node, IVisual layer, Rect clipBounds) @@ -273,11 +288,11 @@ namespace Avalonia.Rendering } } - private void RenderOverlay(Scene scene) + private void RenderOverlay(Scene scene, IDrawingContextImpl parentContent) { if (DrawDirtyRects) { - var overlay = GetOverlay(scene.Size, scene.Scaling); + var overlay = GetOverlay(parentContent, scene.Size, scene.Scaling); using (var context = overlay.CreateDrawingContext(this)) { @@ -301,61 +316,44 @@ namespace Avalonia.Rendering } } - private void RenderComposite(Scene scene) + private void RenderComposite(Scene scene, IDrawingContextImpl context) { - try + var clientRect = new Rect(scene.Size); + + foreach (var layer in scene.Layers) { - if (_renderTarget == null) + var bitmap = _layers[layer.LayerRoot].Bitmap; + var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); + + if (layer.GeometryClip != null) { - _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + context.PushGeometryClip(layer.GeometryClip); } - using (var context = _renderTarget.CreateDrawingContext(this)) + if (layer.OpacityMask == null) { - var clientRect = new Rect(scene.Size); - - foreach (var layer in scene.Layers) - { - var bitmap = _layers[layer.LayerRoot].Bitmap; - var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); - - if (layer.GeometryClip != null) - { - context.PushGeometryClip(layer.GeometryClip); - } - - if (layer.OpacityMask == null) - { - context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect); - } - else - { - context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); - } - - if (layer.GeometryClip != null) - { - context.PopGeometryClip(); - } - } - - if (_overlay != null) - { - var sourceRect = new Rect(0, 0, _overlay.PixelWidth, _overlay.PixelHeight); - context.DrawImage(_overlay, 0.5, sourceRect, clientRect); - } + context.DrawImage(bitmap, layer.Opacity, sourceRect, clientRect); + } + else + { + context.DrawImage(bitmap, layer.OpacityMask, layer.OpacityMaskRect, sourceRect); + } - if (DrawFps) - { - RenderFps(context, clientRect, true); - } + if (layer.GeometryClip != null) + { + context.PopGeometryClip(); } } - catch (RenderTargetCorruptedException ex) + + if (_overlay != null) { - Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); - _renderTarget?.Dispose(); - _renderTarget = null; + var sourceRect = new Rect(0, 0, _overlay.PixelWidth, _overlay.PixelHeight); + context.DrawImage(_overlay, 0.5, sourceRect, clientRect); + } + + if (DrawFps) + { + RenderFps(context, clientRect, true); } } @@ -422,16 +420,19 @@ namespace Avalonia.Rendering } } - private IRenderTargetBitmapImpl GetOverlay(Size size, double scaling) + private IRenderTargetBitmapImpl GetOverlay( + IDrawingContextImpl parentContext, + Size size, + double scaling) { - size = new Size(size.Width * scaling, size.Height * scaling); + var pixelSize = size * scaling; if (_overlay == null || - _overlay.PixelWidth != size.Width || - _overlay.PixelHeight != size.Height) + _overlay.PixelWidth != pixelSize.Width || + _overlay.PixelHeight != pixelSize.Height) { _overlay?.Dispose(); - _overlay = _layerFactory.CreateLayer(null, size, 96 * scaling, 96 * scaling); + _overlay = parentContext.CreateLayer(size); } return _overlay; diff --git a/src/Avalonia.Visuals/Rendering/IRenderLayerFactory.cs b/src/Avalonia.Visuals/Rendering/IRenderLayerFactory.cs deleted file mode 100644 index ed2751cd64..0000000000 --- a/src/Avalonia.Visuals/Rendering/IRenderLayerFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering -{ - public interface IRenderLayerFactory - { - IRenderTargetBitmapImpl CreateLayer(IVisual layerRoot, Size size, double dpiX, double dpiY); - } -} diff --git a/src/Avalonia.Visuals/Rendering/RenderLayer.cs b/src/Avalonia.Visuals/Rendering/RenderLayer.cs index df9497af6f..ed33295db6 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayer.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayer.cs @@ -7,16 +7,16 @@ namespace Avalonia.Rendering { public class RenderLayer { - private readonly IRenderLayerFactory _factory; + private readonly IDrawingContextImpl _drawingContext; public RenderLayer( - IRenderLayerFactory factory, + IDrawingContextImpl drawingContext, Size size, double scaling, IVisual layerRoot) { - _factory = factory; - Bitmap = factory.CreateLayer(layerRoot, size * scaling, 96 * scaling, 96 * scaling); + _drawingContext = drawingContext; + Bitmap = drawingContext.CreateLayer(size); Size = size; Scaling = scaling; LayerRoot = layerRoot; @@ -31,7 +31,7 @@ namespace Avalonia.Rendering { if (Size != size || Scaling != scaling) { - var resized = _factory.CreateLayer(LayerRoot, size * scaling, 96 * scaling, 96 * scaling); + var resized = _drawingContext.CreateLayer(size); using (var context = resized.CreateDrawingContext(null)) { diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs index e1b22c55e0..bafd644603 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.VisualTree; @@ -8,19 +9,17 @@ namespace Avalonia.Rendering { public class RenderLayers : IEnumerable { - private readonly IRenderLayerFactory _factory; private List _inner = new List(); private Dictionary _index = new Dictionary(); - public RenderLayers(IRenderLayerFactory factory) + public RenderLayers() { - _factory = factory; } public int Count => _inner.Count; public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; - public void Update(Scene scene) + public void Update(Scene scene, IDrawingContextImpl context) { for (var i = scene.Layers.Count - 1; i >= 0; --i) { @@ -29,7 +28,7 @@ namespace Avalonia.Rendering if (!_index.TryGetValue(src.LayerRoot, out layer)) { - layer = new RenderLayer(_factory, scene.Size, scene.Scaling, src.LayerRoot); + layer = new RenderLayer(context, scene.Size, scene.Scaling, src.LayerRoot); _inner.Add(layer); _index.Add(src.LayerRoot, layer); } 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/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 0b01960d5b..29c482c336 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -194,6 +194,11 @@ namespace Avalonia.Rendering.SceneGraph } } + public IRenderTargetBitmapImpl CreateLayer(Size size) + { + throw new NotSupportedException("Creating layers on a deferred drawing context not supported"); + } + /// public void PopClip() { 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..aefd873155 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 @@ -65,15 +70,13 @@ namespace Avalonia.Gtk3 Native.GtkMainIteration(); } - public IDisposable StartTimer(TimeSpan interval, Action tick) + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { var msec = interval.TotalMilliseconds; - if (msec <= 0) - throw new ArgumentException("Don't know how to create a timer with zero or negative interval"); var imsec = (uint) msec; if (imsec == 0) imsec = 1; - return GlibTimeout.StarTimer(imsec, tick); + return GlibTimeout.StartTimer(GlibPriority.FromDispatcherPriority(priority), imsec, tick); } private bool[] _signaled = new bool[(int) DispatcherPriority.MaxValue + 1]; 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..0ab4ef980c 100644 --- a/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs +++ b/src/Gtk/Avalonia.Gtk3/Interop/GlibTimeout.cs @@ -48,12 +48,10 @@ namespace Avalonia.Gtk3.Interop } } - public static IDisposable StarTimer(uint interval, Action tick) + public static IDisposable StartTimer(int priority, uint interval, Action tick) { - 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(priority, 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/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs b/src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs index 3aef6944af..e6d21fca36 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs @@ -17,7 +17,7 @@ namespace Avalonia.LinuxFramebuffer public PlatformThreadingInterface() { TlsCurrentThreadIsLoopThread = true; - StartTimer(new TimeSpan(0, 0, 0, 0, 66), () => Tick?.Invoke(this, new EventArgs())); + StartTimer(DispatcherPriority.Render, new TimeSpan(0, 0, 0, 0, 66), () => Tick?.Invoke(this, new EventArgs())); } private readonly AutoResetEvent _signaled = new AutoResetEvent(false); @@ -74,7 +74,7 @@ namespace Avalonia.LinuxFramebuffer } } - public IDisposable StartTimer(TimeSpan interval, Action tick) + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { return new WatTimer(new System.Threading.Timer(delegate { diff --git a/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs b/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs index 80c854f5a5..184416e77a 100644 --- a/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs +++ b/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs @@ -16,7 +16,7 @@ namespace Avalonia.MonoMac public event Action Signaled; - public IDisposable StartTimer(TimeSpan interval, Action tick) + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) => NSTimer.CreateRepeatingScheduledTimer(interval, () => tick()); public void Signal(DispatcherPriority prio) 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..dd3ced1d89 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 @@ -349,6 +354,12 @@ namespace Avalonia.Skia } } + public IRenderTargetBitmapImpl CreateLayer(Size size) + { + var pixelSize = size * (_dpi / 96); + return new BitmapImpl((int)pixelSize.Width, (int)pixelSize.Height, _dpi); + } + public void PushClip(Rect clip) { Canvas.Save(); diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index fd8364c03b..a47c871f5a 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -145,7 +145,9 @@ namespace Avalonia.Direct2D1 return new HwndRenderTarget(nativeWindow); } if (s is IExternalDirect2DRenderTargetSurface external) - return new ExternalRenderTarget(external, s_dwfactory); + return new ExternalRenderTarget(external, s_dwfactory, s_imagingFactory); + 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"); } @@ -156,7 +158,7 @@ namespace Avalonia.Direct2D1 double dpiX, double dpiY) { - return new RenderTargetBitmapImpl( + return new WicRenderTargetBitmapImpl( s_imagingFactory, s_d2D1Factory, s_dwfactory, diff --git a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs index 307048f7b4..176cedd377 100644 --- a/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs @@ -1,9 +1,6 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Direct2D1.Media; +using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Platform; using Avalonia.Rendering; using SharpDX; @@ -11,15 +8,20 @@ using DirectWriteFactory = SharpDX.DirectWrite.Factory; namespace Avalonia.Direct2D1 { - class ExternalRenderTarget : IRenderTarget + class ExternalRenderTarget : IRenderTarget, ILayerFactory { private readonly IExternalDirect2DRenderTargetSurface _externalRenderTargetProvider; private readonly DirectWriteFactory _dwFactory; - public ExternalRenderTarget(IExternalDirect2DRenderTargetSurface externalRenderTargetProvider, - DirectWriteFactory dwFactory) + private readonly SharpDX.WIC.ImagingFactory _wicFactory; + + public ExternalRenderTarget( + IExternalDirect2DRenderTargetSurface externalRenderTargetProvider, + DirectWriteFactory dwFactory, + SharpDX.WIC.ImagingFactory wicFactory) { _externalRenderTargetProvider = externalRenderTargetProvider; _dwFactory = dwFactory; + _wicFactory = wicFactory; } public void Dispose() @@ -31,7 +33,7 @@ namespace Avalonia.Direct2D1 { var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); _externalRenderTargetProvider.BeforeDrawing(); - return new DrawingContextImpl(visualBrushRenderer, target, _dwFactory, null, () => + return new DrawingContextImpl(visualBrushRenderer, null, target, _dwFactory, _wicFactory, null, () => { try { @@ -43,5 +45,15 @@ namespace Avalonia.Direct2D1 } }); } + + public IRenderTargetBitmapImpl CreateLayer(Size size) + { + var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); + return D2DRenderTargetBitmapImpl.CreateCompatible( + _wicFactory, + _dwFactory, + target, + size); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs new file mode 100644 index 0000000000..83bd4d2957 --- /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 : WicRenderTargetBitmapImpl + { + 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/ILayerFactory.cs b/src/Windows/Avalonia.Direct2D1/ILayerFactory.cs new file mode 100644 index 0000000000..99f8d4f7ac --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/ILayerFactory.cs @@ -0,0 +1,10 @@ +using System; +using Avalonia.Platform; + +namespace Avalonia.Direct2D1 +{ + public interface ILayerFactory + { + IRenderTargetBitmapImpl CreateLayer(Size size); + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 69b582b009..6a72923ce3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -2,16 +2,13 @@ // 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 Avalonia.Media; using Avalonia.Platform; -using Avalonia.RenderHelpers; using Avalonia.Rendering; using SharpDX; using SharpDX.Direct2D1; using SharpDX.Mathematics.Interop; -using IBitmap = Avalonia.Media.Imaging.IBitmap; namespace Avalonia.Direct2D1.Media { @@ -21,9 +18,11 @@ namespace Avalonia.Direct2D1.Media public class DrawingContextImpl : IDrawingContextImpl, IDisposable { private readonly IVisualBrushRenderer _visualBrushRenderer; + private readonly ILayerFactory _layerFactory; private readonly SharpDX.Direct2D1.RenderTarget _renderTarget; private readonly SharpDX.DXGI.SwapChain1 _swapChain; private readonly Action _finishedCallback; + private readonly SharpDX.WIC.ImagingFactory _imagingFactory; private SharpDX.DirectWrite.Factory _directWriteFactory; /// @@ -31,21 +30,30 @@ namespace Avalonia.Direct2D1.Media /// /// The visual brush renderer. /// The render target to draw to. + /// + /// An object to use to create layers. May be null, in which case a + /// will created when a new layer is requested. + /// /// The DirectWrite factory. + /// The WIC imaging factory. /// An optional swap chain associated with this drawing context. /// An optional delegate to be called when context is disposed. public DrawingContextImpl( IVisualBrushRenderer visualBrushRenderer, + ILayerFactory layerFactory, SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.DirectWrite.Factory directWriteFactory, + SharpDX.WIC.ImagingFactory imagingFactory, SharpDX.DXGI.SwapChain1 swapChain = null, Action finishedCallback = null) { _visualBrushRenderer = visualBrushRenderer; + _layerFactory = layerFactory; _renderTarget = renderTarget; _swapChain = swapChain; _finishedCallback = finishedCallback; _directWriteFactory = directWriteFactory; + _imagingFactory = imagingFactory; _swapChain = swapChain; _renderTarget.BeginDraw(); } @@ -97,7 +105,7 @@ namespace Avalonia.Direct2D1.Media using (var d2d = ((BitmapImpl)source).GetDirect2DBitmap(_renderTarget)) { _renderTarget.DrawBitmap( - d2d, + d2d.Value, destRect.ToSharpDX(), (float)opacity, BitmapInterpolationMode.Linear, @@ -115,7 +123,7 @@ namespace Avalonia.Direct2D1.Media public void DrawImage(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { using (var d2dSource = ((BitmapImpl)source).GetDirect2DBitmap(_renderTarget)) - using (var sourceBrush = new BitmapBrush(_renderTarget, d2dSource)) + using (var sourceBrush = new BitmapBrush(_renderTarget, d2dSource.Value)) using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size)) using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(_renderTarget.Factory, destRect.ToDirect2D())) { @@ -284,6 +292,25 @@ namespace Avalonia.Direct2D1.Media } } + public IRenderTargetBitmapImpl CreateLayer(Size size) + { + if (_layerFactory != null) + { + return _layerFactory.CreateLayer(size); + } + else + { + var platform = AvaloniaLocator.Current.GetService(); + var dpi = new Vector(_renderTarget.DotsPerInch.Width, _renderTarget.DotsPerInch.Height); + var pixelSize = size * (dpi / 96); + return platform.CreateRenderTargetBitmap( + (int)pixelSize.Width, + (int)pixelSize.Height, + dpi.X, + dpi.Y); + } + } + /// /// Pushes a clip rectange. /// @@ -397,7 +424,7 @@ namespace Avalonia.Direct2D1.Media return new ImageBrushImpl( visualBrush, _renderTarget, - new D2DBitmapImpl(intermediate.Bitmap), + new D2DBitmapImpl(_imagingFactory, intermediate.Bitmap), destinationSize); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index ed3d78b4fd..08cfed2ace 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -10,6 +10,8 @@ namespace Avalonia.Direct2D1.Media { public sealed class ImageBrushImpl : BrushImpl { + OptionalDispose _bitmap; + public ImageBrushImpl( ITileBrush brush, SharpDX.Direct2D1.RenderTarget target, @@ -20,9 +22,10 @@ namespace Avalonia.Direct2D1.Media if (!calc.NeedsIntermediate) { + _bitmap = bitmap.GetDirect2DBitmap(target); PlatformBrush = new BitmapBrush( target, - bitmap.GetDirect2DBitmap(target), + _bitmap.Value, GetBitmapBrushProperties(brush), GetBrushProperties(brush, calc.DestinationRect)); } @@ -41,7 +44,7 @@ namespace Avalonia.Direct2D1.Media public override void Dispose() { - ((BitmapBrush)PlatformBrush)?.Bitmap.Dispose(); + _bitmap.Dispose(); base.Dispose(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs index 63596bdf54..d58f023391 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/BitmapImpl.cs @@ -1,20 +1,38 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Avalonia.Platform; -using SharpDX.Direct2D1; +using SharpDX.WIC; +using D2DBitmap = SharpDX.Direct2D1.Bitmap; namespace Avalonia.Direct2D1.Media { public abstract class BitmapImpl : IBitmapImpl, IDisposable { - public abstract Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target); + public BitmapImpl(ImagingFactory imagingFactory) + { + WicImagingFactory = imagingFactory; + } + + public ImagingFactory WicImagingFactory { get; } public abstract int PixelWidth { get; } public abstract int PixelHeight { get; } - public abstract void Save(string fileName); + + public abstract OptionalDispose GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target); + + public void Save(string fileName) + { + if (Path.GetExtension(fileName) != ".png") + { + // Yeah, we need to support other formats. + throw new NotSupportedException("Use PNG, stoopid."); + } + + using (FileStream s = new FileStream(fileName, FileMode.Create)) + { + Save(s); + } + } + public abstract void Save(Stream stream); public virtual void Dispose() diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs index 5378ae3257..b03e022674 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DBitmapImpl.cs @@ -1,11 +1,10 @@ using System; -using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Avalonia.Platform; using SharpDX.Direct2D1; +using WICFactory = SharpDX.WIC.ImagingFactory; +using ImagingFactory2 = SharpDX.WIC.ImagingFactory2; +using ImageParameters = SharpDX.WIC.ImageParameters; +using PngBitmapEncoder = SharpDX.WIC.PngBitmapEncoder; namespace Avalonia.Direct2D1.Media { @@ -26,32 +25,42 @@ namespace Avalonia.Direct2D1.Media /// or if the render target is a , /// the device associated with this context, to be renderable. /// - public D2DBitmapImpl(Bitmap d2DBitmap) + public D2DBitmapImpl(WICFactory imagingFactory, Bitmap d2DBitmap) + : base(imagingFactory) { - if (d2DBitmap == null) throw new ArgumentNullException(nameof(d2DBitmap)); - - _direct2D = d2DBitmap; + _direct2D = d2DBitmap ?? throw new ArgumentNullException(nameof(d2DBitmap)); } - - public override Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target) => _direct2D; - + public override int PixelWidth => _direct2D.PixelSize.Width; public override int PixelHeight => _direct2D.PixelSize.Height; - public override void Save(string fileName) + public override void Dispose() { - throw new NotImplementedException(); + base.Dispose(); + _direct2D.Dispose(); } - public override void Save(Stream stream) + public override OptionalDispose GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target) { - throw new NotImplementedException(); + return new OptionalDispose(_direct2D, false); } - public override void Dispose() + public override void Save(Stream stream) { - base.Dispose(); - _direct2D.Dispose(); + using (var encoder = new PngBitmapEncoder(WicImagingFactory, stream)) + using (var frameEncode = new SharpDX.WIC.BitmapFrameEncode(encoder)) + using (var imageEncoder = new SharpDX.WIC.ImageEncoder((ImagingFactory2)WicImagingFactory, null)) + { + var parameters = new ImageParameters( + new PixelFormat(SharpDX.DXGI.Format.R8G8B8A8_UNorm, AlphaMode.Premultiplied), + _direct2D.DotsPerInch.Width, + _direct2D.DotsPerInch.Height, + 0, 0, PixelWidth, PixelHeight); + + imageEncoder.WriteFrame(_direct2D, frameEncode, parameters); + frameEncode.Commit(); + encoder.Commit(); + } } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs new file mode 100644 index 0000000000..2843848fac --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -0,0 +1,68 @@ +using System; +using Avalonia.Platform; +using Avalonia.Rendering; +using SharpDX; +using SharpDX.Direct2D1; +using SharpDX.WIC; +using D2DBitmap = SharpDX.Direct2D1.Bitmap; +using DirectWriteFactory = SharpDX.DirectWrite.Factory; + +namespace Avalonia.Direct2D1.Media.Imaging +{ + public class D2DRenderTargetBitmapImpl : D2DBitmapImpl, IRenderTargetBitmapImpl, ILayerFactory + { + private readonly DirectWriteFactory _dwriteFactory; + private readonly BitmapRenderTarget _target; + + public D2DRenderTargetBitmapImpl( + ImagingFactory imagingFactory, + DirectWriteFactory dwriteFactory, + BitmapRenderTarget target) + : base(imagingFactory, target.Bitmap) + { + _dwriteFactory = dwriteFactory; + _target = target; + } + + public override int PixelWidth => _target.PixelSize.Width; + public override int PixelHeight => _target.PixelSize.Height; + + public static D2DRenderTargetBitmapImpl CreateCompatible( + ImagingFactory imagingFactory, + DirectWriteFactory dwriteFactory, + SharpDX.Direct2D1.RenderTarget renderTarget, + Size size) + { + var bitmapRenderTarget = new BitmapRenderTarget( + renderTarget, + CompatibleRenderTargetOptions.None, + new Size2F((float)size.Width, (float)size.Height)); + return new D2DRenderTargetBitmapImpl(imagingFactory, dwriteFactory, bitmapRenderTarget); + } + + public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) + { + return new DrawingContextImpl( + visualBrushRenderer, + this, + _target, + _dwriteFactory, + WicImagingFactory); + } + + public IRenderTargetBitmapImpl CreateLayer(Size size) + { + return CreateCompatible(WicImagingFactory, _dwriteFactory, _target, size); + } + + public override void Dispose() + { + _target.Dispose(); + } + + public override OptionalDispose GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget target) + { + return new OptionalDispose(_target.Bitmap, false); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index e817dd4812..9b99b4c40a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -3,11 +3,10 @@ using System; using System.IO; -using Avalonia.Platform; using Avalonia.Win32.Interop; -using PixelFormat = SharpDX.WIC.PixelFormat; -using APixelFormat = Avalonia.Platform.PixelFormat; using SharpDX.WIC; +using APixelFormat = Avalonia.Platform.PixelFormat; +using D2DBitmap = SharpDX.Direct2D1.Bitmap; namespace Avalonia.Direct2D1.Media { @@ -16,18 +15,14 @@ namespace Avalonia.Direct2D1.Media /// public class WicBitmapImpl : BitmapImpl { - private readonly ImagingFactory _factory; - - /// /// Initializes a new instance of the class. /// /// The WIC imaging factory to use. /// The filename of the bitmap to load. public WicBitmapImpl(ImagingFactory factory, string fileName) + : base(factory) { - _factory = factory; - using (BitmapDecoder decoder = new BitmapDecoder(factory, fileName, DecodeOptions.CacheOnDemand)) { WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnDemand); @@ -40,9 +35,8 @@ namespace Avalonia.Direct2D1.Media /// The WIC imaging factory to use. /// The stream to read the bitmap from. public WicBitmapImpl(ImagingFactory factory, Stream stream) + : base(factory) { - _factory = factory; - using (BitmapDecoder decoder = new BitmapDecoder(factory, stream, DecodeOptions.CacheOnLoad)) { WicImpl = new Bitmap(factory, decoder.GetFrame(0), BitmapCreateCacheOption.CacheOnLoad); @@ -57,11 +51,11 @@ namespace Avalonia.Direct2D1.Media /// The height of the bitmap. /// Pixel format public WicBitmapImpl(ImagingFactory factory, int width, int height, APixelFormat? pixelFormat = null) + : base(factory) { if (!pixelFormat.HasValue) pixelFormat = APixelFormat.Bgra8888; - _factory = factory; PixelFormat = pixelFormat; WicImpl = new Bitmap( factory, @@ -71,7 +65,8 @@ namespace Avalonia.Direct2D1.Media BitmapCreateCacheOption.CacheOnLoad); } - public WicBitmapImpl(ImagingFactory factory, Platform.PixelFormat format, IntPtr data, int width, int height, int stride) + public WicBitmapImpl(ImagingFactory factory, APixelFormat format, IntPtr data, int width, int height, int stride) + : base(factory) { WicImpl = new Bitmap(factory, width, height, format.ToWic(), BitmapCreateCacheOption.CacheOnDemand); PixelFormat = format; @@ -112,41 +107,23 @@ namespace Avalonia.Direct2D1.Media /// /// The render target. /// The Direct2D bitmap. - public override SharpDX.Direct2D1.Bitmap GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget) + public override OptionalDispose GetDirect2DBitmap(SharpDX.Direct2D1.RenderTarget renderTarget) { - FormatConverter converter = new FormatConverter(_factory); + FormatConverter converter = new FormatConverter(WicImagingFactory); converter.Initialize(WicImpl, SharpDX.WIC.PixelFormat.Format32bppPBGRA); - return SharpDX.Direct2D1.Bitmap.FromWicBitmap(renderTarget, converter); + return new OptionalDispose(D2DBitmap.FromWicBitmap(renderTarget, converter), true); } - /// - /// Saves the bitmap to a file. - /// - /// The filename. - public override void Save(string fileName) + public override void Save(Stream stream) { - if (Path.GetExtension(fileName) != ".png") - { - // Yeah, we need to support other formats. - throw new NotSupportedException("Use PNG, stoopid."); - } - - using (FileStream s = new FileStream(fileName, FileMode.Create)) + using (var encoder = new PngBitmapEncoder(WicImagingFactory, stream)) + using (var frame = new BitmapFrameEncode(encoder)) { - Save(s); + frame.Initialize(); + frame.WriteSource(WicImpl); + frame.Commit(); + encoder.Commit(); } } - - public override void Save(Stream stream) - { - PngBitmapEncoder encoder = new PngBitmapEncoder(_factory); - encoder.Initialize(stream); - - BitmapFrameEncode frame = new BitmapFrameEncode(encoder); - frame.Initialize(); - frame.WriteSource(WicImpl); - frame.Commit(); - encoder.Commit(); - } } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs similarity index 64% rename from src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs rename to src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs index 33736b02cb..0eb2608047 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/RenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs @@ -10,20 +10,21 @@ using DirectWriteFactory = SharpDX.DirectWrite.Factory; namespace Avalonia.Direct2D1.Media { - public class RenderTargetBitmapImpl : WicBitmapImpl, IRenderTargetBitmapImpl + public class WicRenderTargetBitmapImpl : WicBitmapImpl, IRenderTargetBitmapImpl { private readonly DirectWriteFactory _dwriteFactory; private readonly WicRenderTarget _target; - public RenderTargetBitmapImpl( + public WicRenderTargetBitmapImpl( ImagingFactory imagingFactory, Factory d2dFactory, DirectWriteFactory dwriteFactory, 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, null, _target, _dwriteFactory, WicImagingFactory, + finishedCallback: finishedCallback); } } } diff --git a/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs b/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs new file mode 100644 index 0000000000..cd3eee8d25 --- /dev/null +++ b/src/Windows/Avalonia.Direct2D1/OptionalDispose.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Direct2D1 +{ + public struct OptionalDispose : IDisposable where T : IDisposable + { + private readonly bool _dispose; + + public OptionalDispose(T value, bool dispose) + { + Value = value; + _dispose = dispose; + } + + public T Value { get; } + + public void Dispose() + { + if (_dispose) Value?.Dispose(); + } + } +} diff --git a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs index b4c9b49e3f..6086b0c67c 100644 --- a/src/Windows/Avalonia.Direct2D1/RenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/RenderTarget.cs @@ -3,14 +3,16 @@ using System; using Avalonia.Direct2D1.Media; +using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Platform; using Avalonia.Rendering; using SharpDX.Direct2D1; using DwFactory = SharpDX.DirectWrite.Factory; +using WicFactory = SharpDX.WIC.ImagingFactory; namespace Avalonia.Direct2D1 { - public class RenderTarget : IRenderTarget + public class RenderTarget : IRenderTarget, ILayerFactory { /// /// The render target. @@ -25,24 +27,13 @@ namespace Avalonia.Direct2D1 { Direct2DFactory = AvaloniaLocator.Current.GetService(); DirectWriteFactory = AvaloniaLocator.Current.GetService(); + WicFactory = AvaloniaLocator.Current.GetService(); _renderTarget = renderTarget; } - /// - /// Gets the Direct2D factory. - /// - public Factory Direct2DFactory - { - get; - } - - /// - /// Gets the DirectWrite factory. - /// - public DwFactory DirectWriteFactory - { - get; - } + public Factory Direct2DFactory { get; } + public DwFactory DirectWriteFactory { get; } + public WicFactory WicFactory { get; } /// /// Creates a drawing context for a rendering session. @@ -50,7 +41,16 @@ namespace Avalonia.Direct2D1 /// An . public IDrawingContextImpl CreateDrawingContext(IVisualBrushRenderer visualBrushRenderer) { - return new DrawingContextImpl(visualBrushRenderer, _renderTarget, DirectWriteFactory); + return new DrawingContextImpl(visualBrushRenderer, this, _renderTarget, DirectWriteFactory, WicFactory); + } + + public IRenderTargetBitmapImpl CreateLayer(Size size) + { + return D2DRenderTargetBitmapImpl.CreateCompatible( + WicFactory, + DirectWriteFactory, + _renderTarget, + size); } public void Dispose() diff --git a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs index 3c1024e73a..0a23c63498 100644 --- a/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs @@ -10,10 +10,11 @@ using Factory = SharpDX.Direct2D1.Factory; using Factory2 = SharpDX.DXGI.Factory2; using Avalonia.Rendering; using Avalonia.Direct2D1.Media; +using Avalonia.Direct2D1.Media.Imaging; namespace Avalonia.Direct2D1 { - public abstract class SwapChainRenderTarget : IRenderTarget + public abstract class SwapChainRenderTarget : IRenderTarget, ILayerFactory { private Size2 _savedSize; private Size2F _savedDpi; @@ -26,24 +27,12 @@ namespace Avalonia.Direct2D1 D2DDevice = AvaloniaLocator.Current.GetService(); Direct2DFactory = AvaloniaLocator.Current.GetService(); DirectWriteFactory = AvaloniaLocator.Current.GetService(); + WicImagingFactory = AvaloniaLocator.Current.GetService(); } - - /// - /// Gets the Direct2D factory. - /// - public Factory Direct2DFactory - { - get; - } - - /// - /// Gets the DirectWrite factory. - /// - public SharpDX.DirectWrite.Factory DirectWriteFactory - { - get; - } + public Factory Direct2DFactory { get; } + public SharpDX.DirectWrite.Factory DirectWriteFactory { get; } + public SharpDX.WIC.ImagingFactory WicImagingFactory { get; } protected SharpDX.DXGI.Device DxgiDevice { get; } @@ -67,11 +56,27 @@ namespace Avalonia.Direct2D1 return new DrawingContextImpl( visualBrushRenderer, + this, _deviceContext, DirectWriteFactory, + WicImagingFactory, _swapChain); } + public IRenderTargetBitmapImpl CreateLayer(Size size) + { + if (_deviceContext == null) + { + CreateSwapChain(); + } + + return D2DRenderTargetBitmapImpl.CreateCompatible( + WicImagingFactory, + DirectWriteFactory, + _deviceContext, + size); + } + public void Dispose() { _deviceContext?.Dispose(); @@ -86,7 +91,6 @@ namespace Avalonia.Direct2D1 _deviceContext?.Dispose(); _deviceContext = new DeviceContext(D2DDevice, DeviceContextOptions.None) {DotsPerInch = _savedDpi}; - var swapChainDesc = new SwapChainDescription1 { Width = _savedSize.Width, diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index a260efd9b9..e265749249 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -114,7 +114,7 @@ namespace Avalonia.Win32 } } - public IDisposable StartTimer(TimeSpan interval, Action callback) + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action callback) { UnmanagedMethods.TimerProc timerDelegate = (hWnd, uMsg, nIDEvent, dwTime) => callback(); diff --git a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs b/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs index 6d6a5e22ca..43a620cccd 100644 --- a/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs +++ b/src/iOS/Avalonia.iOS/PlatformThreadingInterface.cs @@ -51,7 +51,7 @@ namespace Avalonia.iOS } }*/ - public IDisposable StartTimer(TimeSpan interval, Action tick) + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) => NSTimer.CreateRepeatingScheduledTimer(interval, _ => tick()); public void Signal(DispatcherPriority prio) 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.Base.UnitTests/AvaloniaObjectTests_Threading.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs index 229a34643d..09aedbdf9c 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Threading.cs @@ -160,7 +160,7 @@ namespace Avalonia.Base.UnitTests throw new NotImplementedException(); } - public IDisposable StartTimer(TimeSpan interval, Action tick) + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { throw new NotImplementedException(); } 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.RenderTests/TestBase.cs b/tests/Avalonia.RenderTests/TestBase.cs index 409870ed0f..84860eefdb 100644 --- a/tests/Avalonia.RenderTests/TestBase.cs +++ b/tests/Avalonia.RenderTests/TestBase.cs @@ -161,7 +161,7 @@ namespace Avalonia.Direct2D1.RenderTests throw new NotImplementedException(); } - public IDisposable StartTimer(TimeSpan interval, Action tick) + public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.UnitTests/TestRoot.cs b/tests/Avalonia.UnitTests/TestRoot.cs index 90532d64a1..9ec053f075 100644 --- a/tests/Avalonia.UnitTests/TestRoot.cs +++ b/tests/Avalonia.UnitTests/TestRoot.cs @@ -49,8 +49,6 @@ namespace Avalonia.UnitTests public ILayoutManager LayoutManager => AvaloniaLocator.Current.GetService(); - public IRenderTarget RenderTarget => null; - public IRenderer Renderer { get; set; } public IAccessKeyHandler AccessKeyHandler => null; diff --git a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs index a9b27ed601..cec95d4807 100644 --- a/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Rendering/DeferredRendererTests.cs @@ -30,7 +30,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering root, loop.Object, sceneBuilder: MockSceneBuilder(root).Object, - layerFactory: MockLayerFactory(root).Object, dispatcher: dispatcher.Object); target.Start(); @@ -55,7 +54,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder.Object, - layerFactory: MockLayerFactory(root).Object, dispatcher: dispatcher); target.Start(); @@ -75,7 +73,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder.Object, - layerFactory: MockLayerFactory(root).Object, dispatcher: dispatcher); target.Start(); @@ -111,7 +108,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering root, loop.Object, sceneBuilder: sceneBuilder.Object, - layerFactory: MockLayerFactory(root).Object, dispatcher: dispatcher); target.Start(); @@ -146,22 +142,20 @@ namespace Avalonia.Visuals.UnitTests.Rendering scene.Layers.Add(root).Dirty.Add(new Rect(root.ClientSize)); }); - var layers = new Mock(); - layers.Setup(x => x.CreateLayer(root, root.ClientSize, 96, 96)).Returns(CreateLayer()); - var renderInterface = new Mock(); var target = new DeferredRenderer( root, loop.Object, sceneBuilder: sceneBuilder.Object, - layerFactory: layers.Object, + //layerFactory: layers.Object, dispatcher: dispatcher); target.Start(); RunFrame(loop); - layers.Verify(x => x.CreateLayer(root, root.ClientSize, 96, 96)); + var context = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null)); + context.Verify(x => x.CreateLayer(root.ClientSize)); } [Fact] @@ -185,25 +179,25 @@ namespace Avalonia.Visuals.UnitTests.Rendering root.Measure(Size.Infinity); root.Arrange(new Rect(root.DesiredSize)); - var loop = new Mock(); - var layerFactory = new MockRenderLayerFactory(new Dictionary - { - { root, CreateLayer() }, - { border, CreateLayer() }, - }); + var rootLayer = CreateLayer(); + var borderLayer = CreateLayer(); + var renderTargetContext = Mock.Get(root.CreateRenderTarget().CreateDrawingContext(null)); + renderTargetContext.SetupSequence(x => x.CreateLayer(It.IsAny())) + .Returns(rootLayer) + .Returns(borderLayer); + var loop = new Mock(); var target = new DeferredRenderer( - root, + root, loop.Object, - layerFactory: layerFactory, dispatcher: new ImmediateDispatcher()); root.Renderer = target; target.Start(); RunFrame(loop); - var rootContext = layerFactory.GetMockDrawingContext(root); - var borderContext = layerFactory.GetMockDrawingContext(border); + var rootContext = Mock.Get(rootLayer.CreateDrawingContext(null)); + var borderContext = Mock.Get(borderLayer.CreateDrawingContext(null)); rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once); rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once); @@ -223,7 +217,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering border.Opacity = 1; RunFrame(loop); - layerFactory.GetMockBitmap(border).Verify(x => x.Dispose()); + Mock.Get(borderLayer).Verify(x => x.Dispose()); rootContext.Verify(x => x.FillRectangle(Brushes.Red, new Rect(0, 0, 100, 100), 0), Times.Once); rootContext.Verify(x => x.FillRectangle(Brushes.Green, new Rect(0, 0, 100, 100), 0), Times.Once); borderContext.Verify(x => x.FillRectangle(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); @@ -246,13 +240,6 @@ namespace Avalonia.Visuals.UnitTests.Rendering x.CreateDrawingContext(It.IsAny()) == Mock.Of()); } - private Mock MockLayerFactory(IRenderRoot root) - { - var result = new Mock(); - result.Setup(x => x.CreateLayer(root, root.ClientSize, 96, 96)).Returns(CreateLayer()); - return result; - } - private Mock MockSceneBuilder(IRenderRoot root) { var result = new Mock(); @@ -260,34 +247,5 @@ namespace Avalonia.Visuals.UnitTests.Rendering .Callback(x => x.Layers.Add(root).Dirty.Add(new Rect(root.ClientSize))); return result; } - - private class MockRenderLayerFactory : IRenderLayerFactory - { - private IDictionary _layers; - - public MockRenderLayerFactory(IDictionary layers) - { - _layers = layers; - } - - public IRenderTargetBitmapImpl CreateLayer( - IVisual layerRoot, - Size size, - double dpiX, - double dpiY) - { - return _layers[layerRoot]; - } - - public Mock GetMockBitmap(IVisual layerRoot) - { - return Mock.Get(_layers[layerRoot]); - } - - public Mock GetMockDrawingContext(IVisual layerRoot) - { - return Mock.Get(_layers[layerRoot].CreateDrawingContext(null)); - } - } } } 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