diff --git a/build/ReactiveUI.props b/build/ReactiveUI.props index f74ab07e31..c3b136d41d 100644 --- a/build/ReactiveUI.props +++ b/build/ReactiveUI.props @@ -1,5 +1,5 @@ - + diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index d627a2bf19..eec21d4423 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -301,14 +301,19 @@ partial class Build : NukeBuild .Executes(() => { var data = Parameters; + var pathToProjectSource = RootDirectory / "samples" / "ControlCatalog.NetCore"; + var pathToPublish = pathToProjectSource / "bin" / data.Configuration / "publish"; + + DotNetPublish(c => c + .SetProject(pathToProjectSource / "ControlCatalog.NetCore.csproj") + .EnableNoBuild() + .SetConfiguration(data.Configuration) + .AddProperty("PackageVersion", data.Version) + .AddProperty("PublishDir", pathToPublish)); + Zip(data.ZipCoreArtifacts, data.BinRoot); Zip(data.ZipNuGetArtifacts, data.NugetRoot); - Zip(data.ZipTargetControlCatalogDesktopDir, - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dll").Concat( - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.config")).Concat( - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.so")).Concat( - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.dylib")).Concat( - GlobFiles(data.ZipSourceControlCatalogDesktopDir, "*.exe"))); + Zip(data.ZipTargetControlCatalogNetCoreDir, pathToPublish); }); Target CreateIntermediateNugetPackages => _ => _ diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index c76019d9eb..a92c988fbd 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -58,8 +58,7 @@ public partial class Build public string FileZipSuffix { get; } public AbsolutePath ZipCoreArtifacts { get; } public AbsolutePath ZipNuGetArtifacts { get; } - public AbsolutePath ZipSourceControlCatalogDesktopDir { get; } - public AbsolutePath ZipTargetControlCatalogDesktopDir { get; } + public AbsolutePath ZipTargetControlCatalogNetCoreDir { get; } public BuildParameters(Build b) @@ -129,9 +128,7 @@ public partial class Build FileZipSuffix = Version + ".zip"; ZipCoreArtifacts = ZipRoot / ("Avalonia-" + FileZipSuffix); ZipNuGetArtifacts = ZipRoot / ("Avalonia-NuGet-" + FileZipSuffix); - ZipSourceControlCatalogDesktopDir = - RootDirectory / ("samples/ControlCatalog.Desktop/bin/" + DirSuffix + "/net461"); - ZipTargetControlCatalogDesktopDir = ZipRoot / ("ControlCatalog.Desktop-" + FileZipSuffix); + ZipTargetControlCatalogNetCoreDir = ZipRoot / ("ControlCatalog.NetCore-" + FileZipSuffix); } string GetVersion() diff --git a/readme.md b/readme.md index f73bdffaeb..67b706f428 100644 --- a/readme.md +++ b/readme.md @@ -2,8 +2,6 @@
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg) -Avalonia - ## 📖 About AvaloniaUI Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index d5aedf7783..3c2d2ee359 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -1,7 +1,7 @@  - Exe + WinExe netcoreapp3.1 true @@ -15,6 +15,10 @@ + + + en + diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 1aa926a2a6..53ad213d92 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -27,6 +27,6 @@ - + diff --git a/samples/RenderDemo/MainWindow.xaml b/samples/RenderDemo/MainWindow.xaml index 93fbe5e412..aa165d13f7 100644 --- a/samples/RenderDemo/MainWindow.xaml +++ b/samples/RenderDemo/MainWindow.xaml @@ -57,6 +57,9 @@ + + + diff --git a/samples/RenderDemo/Pages/PathMeasurementPage.cs b/samples/RenderDemo/Pages/PathMeasurementPage.cs new file mode 100644 index 0000000000..212377deae --- /dev/null +++ b/samples/RenderDemo/Pages/PathMeasurementPage.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics; +using System.Drawing.Drawing2D; +using System.Security.Cryptography; +using Avalonia; +using Avalonia.Controls; +using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Threading; +using Avalonia.Visuals.Media.Imaging; + +namespace RenderDemo.Pages +{ + public class PathMeasurementPage : Control + { + static PathMeasurementPage() + { + AffectsRender(BoundsProperty); + } + + private RenderTargetBitmap _bitmap; + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _bitmap = new RenderTargetBitmap(new PixelSize(500, 500), new Vector(96, 96)); + base.OnAttachedToLogicalTree(e); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + _bitmap.Dispose(); + _bitmap = null; + base.OnDetachedFromLogicalTree(e); + } + + readonly IPen strokePen = new ImmutablePen(Brushes.DarkBlue, 10d, null, PenLineCap.Round, PenLineJoin.Round); + readonly IPen strokePen1 = new ImmutablePen(Brushes.Purple, 10d, null, PenLineCap.Round, PenLineJoin.Round); + readonly IPen strokePen2 = new ImmutablePen(Brushes.Green, 10d, null, PenLineCap.Round, PenLineJoin.Round); + readonly IPen strokePen3 = new ImmutablePen(Brushes.LightBlue, 10d, null, PenLineCap.Round, PenLineJoin.Round); + readonly IPen strokePen4 = new ImmutablePen(Brushes.Red, 1d, null, PenLineCap.Round, PenLineJoin.Round); + + public override void Render(DrawingContext context) + { + using (var ctxi = _bitmap.CreateDrawingContext(null)) + using (var bitmapCtx = new DrawingContext(ctxi, false)) + { + ctxi.Clear(default); + + var basePath = new PathGeometry(); + + using (var basePathCtx = basePath.Open()) + { + basePathCtx.BeginFigure(new Point(20, 20), false); + basePathCtx.LineTo(new Point(400, 50)); + basePathCtx.LineTo(new Point(80, 100)); + basePathCtx.LineTo(new Point(300, 150)); + basePathCtx.EndFigure(false); + } + + bitmapCtx.DrawGeometry(null, strokePen, basePath); + + + var length = basePath.PlatformImpl.ContourLength; + + if (basePath.PlatformImpl.TryGetSegment(length * 0.05, length * 0.2, true, out var dst1)) + bitmapCtx.DrawGeometry(null, strokePen1, dst1); + + if (basePath.PlatformImpl.TryGetSegment(length * 0.2, length * 0.8, true, out var dst2)) + bitmapCtx.DrawGeometry(null, strokePen2, dst2); + + if (basePath.PlatformImpl.TryGetSegment(length * 0.8, length * 0.95, true, out var dst3)) + bitmapCtx.DrawGeometry(null, strokePen3, dst3); + + var pathBounds = basePath.GetRenderBounds(strokePen); + + bitmapCtx.DrawRectangle(null, strokePen4, pathBounds); + } + + + context.DrawImage(_bitmap, + new Rect(0, 0, 500, 500), + new Rect(0, 0, 500, 500)); + + base.Render(context); + } + } +} diff --git a/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs new file mode 100644 index 0000000000..f207b558a3 --- /dev/null +++ b/src/Avalonia.Build.Tasks/DeterministicIdGenerator.cs @@ -0,0 +1,12 @@ +using System; +using XamlX.Transform; + +namespace Avalonia.Build.Tasks +{ + public class DeterministicIdGenerator : IXamlIdentifierGenerator + { + private int _nextId = 1; + + public string GenerateIdentifierPart() => (_nextId++).ToString(); + } +} diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 6ef8a98fae..508045dccb 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -22,7 +22,6 @@ using XamlX.IL; namespace Avalonia.Build.Tasks { - public static partial class XamlCompilerTaskExecutor { static bool CheckXamlName(IResource r) => r.Name.ToLowerInvariant().EndsWith(".xaml") @@ -99,7 +98,8 @@ namespace Avalonia.Build.Tasks XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), AvaloniaXamlIlLanguage.CustomValueConverter, new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)), - new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure))); + new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure)), + new DeterministicIdGenerator()); var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", diff --git a/src/Avalonia.Controls/ApiCompatBaseline.txt b/src/Avalonia.Controls/ApiCompatBaseline.txt index 0284463f1c..3a6810eed9 100644 --- a/src/Avalonia.Controls/ApiCompatBaseline.txt +++ b/src/Avalonia.Controls/ApiCompatBaseline.txt @@ -3,6 +3,7 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Control InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.INativeMenuExporterEventsImplBridge.RaiseOpening()' is present in the implementation but not in the contract. MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract. +EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract. InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation. MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract. diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index c779e4b0cb..6093cbd581 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -218,6 +218,7 @@ namespace Avalonia.Controls if (Command != null) { Command.CanExecuteChanged += CanExecuteChanged; + CanExecuteChanged(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index cd60130c5b..be15d3d444 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -14,17 +14,21 @@ namespace Avalonia.Controls.Chrome public class CaptionButtons : TemplatedControl { private CompositeDisposable? _disposables; - private Window? _hostWindow; - public void Attach(Window hostWindow) + /// + /// Currently attached window. + /// + protected Window? HostWindow { get; private set; } + + public virtual void Attach(Window hostWindow) { if (_disposables == null) { - _hostWindow = hostWindow; + HostWindow = hostWindow; _disposables = new CompositeDisposable { - _hostWindow.GetObservable(Window.WindowStateProperty) + HostWindow.GetObservable(Window.WindowStateProperty) .Subscribe(x => { PseudoClasses.Set(":minimized", x == WindowState.Minimized); @@ -36,14 +40,45 @@ namespace Avalonia.Controls.Chrome } } - public void Detach() + public virtual void Detach() { if (_disposables != null) { _disposables.Dispose(); _disposables = null; - _hostWindow = null; + HostWindow = null; + } + } + + protected virtual void OnClose() + { + HostWindow?.Close(); + } + + protected virtual void OnRestore() + { + if (HostWindow != null) + { + HostWindow.WindowState = HostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + } + + protected virtual void OnMinimize() + { + if (HostWindow != null) + { + HostWindow.WindowState = WindowState.Minimized; + } + } + + protected virtual void OnToggleFullScreen() + { + if (HostWindow != null) + { + HostWindow.WindowState = HostWindow.WindowState == WindowState.FullScreen + ? WindowState.Normal + : WindowState.FullScreen; } } @@ -56,31 +91,13 @@ namespace Avalonia.Controls.Chrome var minimiseButton = e.NameScope.Get("PART_MinimiseButton"); var fullScreenButton = e.NameScope.Get("PART_FullScreenButton"); - closeButton.PointerReleased += (sender, e) => _hostWindow?.Close(); + closeButton.PointerReleased += (sender, e) => OnClose(); - restoreButton.PointerReleased += (sender, e) => - { - if (_hostWindow != null) - { - _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; - } - }; + restoreButton.PointerReleased += (sender, e) => OnRestore(); - minimiseButton.PointerReleased += (sender, e) => - { - if (_hostWindow != null) - { - _hostWindow.WindowState = WindowState.Minimized; - } - }; + minimiseButton.PointerReleased += (sender, e) => OnMinimize(); - fullScreenButton.PointerReleased += (sender, e) => - { - if (_hostWindow != null) - { - _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen; - } - }; + fullScreenButton.PointerReleased += (sender, e) => OnToggleFullScreen(); } } } diff --git a/src/Avalonia.Controls/NativeControlHost.cs b/src/Avalonia.Controls/NativeControlHost.cs index 64414b1f47..c2fe1bb691 100644 --- a/src/Avalonia.Controls/NativeControlHost.cs +++ b/src/Avalonia.Controls/NativeControlHost.cs @@ -16,30 +16,16 @@ namespace Avalonia.Controls private bool _queuedForDestruction; private bool _queuedForMoveResize; private readonly List _propertyChangedSubscriptions = new List(); - private readonly EventHandler _propertyChangedHandler; - static NativeControlHost() - { - IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged); - } - - public NativeControlHost() - { - _propertyChangedHandler = PropertyChangedHandler; - } - - private static void OnVisibleChanged(NativeControlHost host, AvaloniaPropertyChangedEventArgs arg2) - => host.UpdateHost(); protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { _currentRoot = e.Root as TopLevel; var visual = (IVisual)this; - while (visual != _currentRoot) + while (visual != null) { - if (visual is Visual v) { - v.PropertyChanged += _propertyChangedHandler; + v.PropertyChanged += PropertyChangedHandler; _propertyChangedSubscriptions.Add(v); } @@ -51,7 +37,7 @@ namespace Avalonia.Controls private void PropertyChangedHandler(object sender, AvaloniaPropertyChangedEventArgs e) { - if (e.IsEffectiveValueChange && e.Property == BoundsProperty) + if (e.IsEffectiveValueChange && (e.Property == BoundsProperty || e.Property == IsVisibleProperty)) EnqueueForMoveResize(); } @@ -61,7 +47,7 @@ namespace Avalonia.Controls if (_propertyChangedSubscriptions != null) { foreach (var v in _propertyChangedSubscriptions) - v.PropertyChanged -= _propertyChangedHandler; + v.PropertyChanged -= PropertyChangedHandler; _propertyChangedSubscriptions.Clear(); } UpdateHost(); diff --git a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs index de3f58886b..bb3c0288eb 100644 --- a/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs +++ b/src/Avalonia.Controls/Platform/ExtendClientAreaChromeHints.cs @@ -16,7 +16,7 @@ namespace Avalonia.Platform /// /// The default for the platform. /// - Default = SystemChrome, + Default = PreferSystemChrome, /// /// Use SystemChrome diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index e02efc2bd2..6419981fb1 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -341,7 +341,9 @@ namespace Avalonia.Controls var pointNum = orient ? x.Position.X : x.Position.Y; var logicalPos = MathUtilities.Clamp(pointNum / pointDen, 0.0d, 1.0d); - var invert = orient ? 0 : 1; + var invert = orient ? + IsDirectionReversed ? 1 : 0 : + IsDirectionReversed ? 0 : 1; var calcVal = Math.Abs(invert - logicalPos); var range = Maximum - Minimum; var finalValue = calcVal * range + Minimum; diff --git a/src/Avalonia.Diagnostics/DevToolsExtensions.cs b/src/Avalonia.Diagnostics/DevToolsExtensions.cs index 4bc2ca313f..aa585dca40 100644 --- a/src/Avalonia.Diagnostics/DevToolsExtensions.cs +++ b/src/Avalonia.Diagnostics/DevToolsExtensions.cs @@ -15,7 +15,7 @@ namespace Avalonia /// The window to attach DevTools to. public static void AttachDevTools(this TopLevel root) { - DevTools.Attach(root, new KeyGesture(Key.F12)); + DevTools.Attach(root, new DevToolsOptions()); } /// @@ -27,5 +27,15 @@ namespace Avalonia { DevTools.Attach(root, gesture); } + + /// + /// Attaches DevTools to a window, to be opened with the specified options. + /// + /// The window to attach DevTools to. + /// Additional settings of DevTools. + public static void AttachDevTools(this TopLevel root, DevToolsOptions options) + { + DevTools.Attach(root, options); + } } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs index 4899be2955..7942d22962 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/DevTools.cs @@ -6,6 +6,8 @@ using Avalonia.Diagnostics.Views; using Avalonia.Input; using Avalonia.Interactivity; +#nullable enable + namespace Avalonia.Diagnostics { public static class DevTools @@ -13,12 +15,20 @@ namespace Avalonia.Diagnostics private static readonly Dictionary s_open = new Dictionary(); public static IDisposable Attach(TopLevel root, KeyGesture gesture) + { + return Attach(root, new DevToolsOptions() + { + Gesture = gesture, + }); + } + + public static IDisposable Attach(TopLevel root, DevToolsOptions options) { void PreviewKeyDown(object sender, KeyEventArgs e) { - if (gesture.Matches(e)) + if (options.Gesture.Matches(e)) { - Open(root); + Open(root, options); } } @@ -28,7 +38,9 @@ namespace Avalonia.Diagnostics RoutingStrategies.Tunnel); } - public static IDisposable Open(TopLevel root) + public static IDisposable Open(TopLevel root) => Open(root, new DevToolsOptions()); + + public static IDisposable Open(TopLevel root, DevToolsOptions options) { if (s_open.TryGetValue(root, out var window)) { @@ -38,15 +50,15 @@ namespace Avalonia.Diagnostics { window = new MainWindow { - Width = 1024, - Height = 512, Root = root, + Width = options.Size.Width, + Height = options.Size.Height, }; window.Closed += DevToolsClosed; s_open.Add(root, window); - if (root is Window inspectedWindow) + if (options.ShowAsChildWindow && root is Window inspectedWindow) { window.Show(inspectedWindow); } diff --git a/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs new file mode 100644 index 0000000000..ee46192207 --- /dev/null +++ b/src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs @@ -0,0 +1,26 @@ +using Avalonia.Input; + +namespace Avalonia.Diagnostics +{ + /// + /// Describes options used to customize DevTools. + /// + public class DevToolsOptions + { + /// + /// Gets or sets the key gesture used to open DevTools. + /// + public KeyGesture Gesture { get; set; } = new KeyGesture(Key.F12); + + /// + /// Gets or sets a value indicating whether DevTools should be displayed as a child window + /// of the window being inspected. The default value is true. + /// + public bool ShowAsChildWindow { get; set; } = true; + + /// + /// Gets or sets the initial size of the DevTools window. The default value is 1024x512. + /// + public Size Size { get; set; } = new Size(1024, 512); + } +} diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 6a78f4c6e7..62cac378d7 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -104,6 +104,9 @@ namespace Avalonia.Headless } public Rect Bounds { get; set; } + + public double ContourLength { get; } = 0; + public virtual bool FillContains(Point point) => Bounds.Contains(point); public Rect GetRenderBounds(IPen pen) @@ -126,6 +129,25 @@ namespace Avalonia.Headless public ITransformedGeometryImpl WithTransform(Matrix transform) => new HeadlessTransformedGeometryStub(this, transform); + + public bool TryGetPointAtDistance(double distance, out Point point) + { + point = new Point(); + return false; + } + + public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) + { + point = new Point(); + tangent = new Point(); + return false; + } + + public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) + { + segmentGeometry = null; + return false; + } } class HeadlessTransformedGeometryStub : HeadlessGeometryStub, ITransformedGeometryImpl @@ -359,6 +381,16 @@ namespace Avalonia.Headless } + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + + } + + public void PopBitmapBlendMode() + { + + } + public void Custom(ICustomDrawOperation custom) { diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 1be2595ebe..60b6b9e947 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -24,7 +24,7 @@ namespace Avalonia.Input public static readonly RoutedEvent ScrollGestureEvent = RoutedEvent.Register( "ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures)); - + public static readonly RoutedEvent ScrollGestureEndedEvent = RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); @@ -89,7 +89,7 @@ namespace Avalonia.Input { if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { - e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); + e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e)); } } } @@ -105,8 +105,14 @@ namespace Avalonia.Input { if (e.InitialPressMouseButton == MouseButton.Left || e.InitialPressMouseButton == MouseButton.Right) { - var et = e.InitialPressMouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent; - e.Source.RaiseEvent(new RoutedEventArgs(et)); + if (e.InitialPressMouseButton == MouseButton.Right) + { + e.Source.RaiseEvent(new TappedEventArgs(RightTappedEvent, e)); + } + else + { + e.Source.RaiseEvent(new TappedEventArgs(TappedEvent, e)); + } } } } diff --git a/src/Avalonia.Input/PointerEventArgs.cs b/src/Avalonia.Input/PointerEventArgs.cs index 451f80b1df..ba39f7ca8e 100644 --- a/src/Avalonia.Input/PointerEventArgs.cs +++ b/src/Avalonia.Input/PointerEventArgs.cs @@ -107,7 +107,9 @@ namespace Avalonia.Input None, Left, Right, - Middle + Middle, + XButton1, + XButton2 } public class PointerPressedEventArgs : PointerEventArgs diff --git a/src/Avalonia.Input/PointerPoint.cs b/src/Avalonia.Input/PointerPoint.cs index 37d2120d03..9f8285a8e1 100644 --- a/src/Avalonia.Input/PointerPoint.cs +++ b/src/Avalonia.Input/PointerPoint.cs @@ -90,6 +90,10 @@ namespace Avalonia.Input return MouseButton.Middle; if (kind == PointerUpdateKind.RightButtonPressed || kind == PointerUpdateKind.RightButtonReleased) return MouseButton.Right; + if (kind == PointerUpdateKind.XButton1Pressed || kind == PointerUpdateKind.XButton1Released) + return MouseButton.XButton1; + if (kind == PointerUpdateKind.XButton2Pressed || kind == PointerUpdateKind.XButton2Released) + return MouseButton.XButton2; return MouseButton.None; } } diff --git a/src/Avalonia.Input/TappedEventArgs.cs b/src/Avalonia.Input/TappedEventArgs.cs new file mode 100644 index 0000000000..02add509cd --- /dev/null +++ b/src/Avalonia.Input/TappedEventArgs.cs @@ -0,0 +1,18 @@ +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Avalonia.Input +{ + public class TappedEventArgs : RoutedEventArgs + { + private readonly PointerEventArgs lastPointerEventArgs; + + public TappedEventArgs(RoutedEvent routedEvent, PointerEventArgs lastPointerEventArgs) + : base(routedEvent) + { + this.lastPointerEventArgs = lastPointerEventArgs; + } + + public Point GetPosition(IVisual? relativeTo) => lastPointerEventArgs.GetPosition(relativeTo); + } +} diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index 9cada1120a..adcbeb2d3a 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -397,7 +397,7 @@ enum AvnExtendClientAreaChromeHints AvnSystemChrome = 0x01, AvnPreferSystemChrome = 0x02, AvnOSXThickTitleBar = 0x08, - AvnDefaultChrome = AvnSystemChrome, + AvnDefaultChrome = AvnPreferSystemChrome, } [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)] diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index 6771d3e179..359da3d7c2 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -9,18 +9,22 @@ namespace Avalonia.ReactiveUI { /// /// Initializes ReactiveUI framework to use with Avalonia. Registers Avalonia - /// scheduler and Avalonia activation for view fetcher. Always remember to - /// call this method if you are using ReactiveUI in your application. + /// scheduler, an activation for view fetcher, a template binding hook. Remember + /// to call this method if you are using ReactiveUI in your application. /// public static TAppBuilder UseReactiveUI(this TAppBuilder builder) - where TAppBuilder : AppBuilderBase, new() - { - return builder.AfterPlatformServicesSetup(_ => + where TAppBuilder : AppBuilderBase, new() => + builder.AfterPlatformServicesSetup(_ => Locator.RegisterResolverCallbackChanged(() => { + if (Locator.CurrentMutable is null) + { + return; + } + + PlatformRegistrationManager.SetRegistrationNamespaces(RegistrationNamespace.Avalonia); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook)); - }); - } + })); } } diff --git a/src/Avalonia.Styling/ClassBindingManager.cs b/src/Avalonia.Styling/ClassBindingManager.cs new file mode 100644 index 0000000000..e8b1cc301d --- /dev/null +++ b/src/Avalonia.Styling/ClassBindingManager.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data; + +namespace Avalonia +{ + internal static class ClassBindingManager + { + private static readonly Dictionary s_RegisteredProperties = + new Dictionary(); + + public static IDisposable Bind(IStyledElement target, string className, IBinding source, object anchor) + { + if (!s_RegisteredProperties.TryGetValue(className, out var prop)) + s_RegisteredProperties[className] = prop = RegisterClassProxyProperty(className); + return target.Bind(prop, source, anchor); + } + + private static AvaloniaProperty RegisterClassProxyProperty(string className) + { + var prop = AvaloniaProperty.Register("__AvaloniaReserved::Classes::" + className); + prop.Changed.Subscribe(args => + { + var classes = ((IStyledElement)args.Sender).Classes; + classes.Set(className, args.NewValue.GetValueOrDefault()); + }); + + return prop; + } + } +} diff --git a/src/Avalonia.Styling/Controls/Classes.cs b/src/Avalonia.Styling/Controls/Classes.cs index 51dca57928..4e2783d4ec 100644 --- a/src/Avalonia.Styling/Controls/Classes.cs +++ b/src/Avalonia.Styling/Controls/Classes.cs @@ -265,5 +265,26 @@ namespace Avalonia.Controls $"The pseudoclass '{name}' may only be {operation} by the control itself."); } } + + /// + /// Adds a or removes a style class to/from the collection. + /// + /// The class names. + /// If true adds the class, if false, removes it. + /// + /// Only standard classes may be added or removed via this method. To add pseudoclasses (classes + /// beginning with a ':' character) use the protected + /// property. + /// + public void Set(string name, bool value) + { + if (value) + { + if (!Contains(name)) + Add(name); + } + else + Remove(name); + } } } diff --git a/src/Avalonia.Styling/StyledElementExtensions.cs b/src/Avalonia.Styling/StyledElementExtensions.cs new file mode 100644 index 0000000000..0c5a5f7438 --- /dev/null +++ b/src/Avalonia.Styling/StyledElementExtensions.cs @@ -0,0 +1,11 @@ +using System; +using Avalonia.Data; + +namespace Avalonia +{ + public static class StyledElementExtensions + { + public static IDisposable BindClass(this IStyledElement target, string className, IBinding source, object anchor) => + ClassBindingManager.Bind(target, className, source, anchor); + } +} diff --git a/src/Avalonia.Visuals/ApiCompatBaseline.txt b/src/Avalonia.Visuals/ApiCompatBaseline.txt new file mode 100644 index 0000000000..805d1955ea --- /dev/null +++ b/src/Avalonia.Visuals/ApiCompatBaseline.txt @@ -0,0 +1,9 @@ +Compat issues with assembly Avalonia.Visuals: +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PopBitmapBlendMode()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IDrawingContextImpl.PushBitmapBlendMode(Avalonia.Visuals.Media.Imaging.BitmapBlendingMode)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avalonia.Platform.IGeometryImpl.ContourLength.get()' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract. +InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract. +Total Issues: 7 diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index ae4c927ae2..4e3dc8699c 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -121,12 +121,23 @@ namespace Avalonia.Media /// The stroke pen. /// The geometry. public void DrawGeometry(IBrush brush, IPen pen, Geometry geometry) + { + DrawGeometry(brush, pen, geometry.PlatformImpl); + } + + /// + /// Draws a geometry. + /// + /// The fill brush. + /// The stroke pen. + /// The geometry. + public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry) { Contract.Requires(geometry != null); if (brush != null || PenIsVisible(pen)) { - PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl); + PlatformImpl.DrawGeometry(brush, pen, geometry); } } diff --git a/src/Avalonia.Visuals/Media/Imaging/BitmapBlendingMode.cs b/src/Avalonia.Visuals/Media/Imaging/BitmapBlendingMode.cs new file mode 100644 index 0000000000..473b43dab3 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/BitmapBlendingMode.cs @@ -0,0 +1,57 @@ +namespace Avalonia.Visuals.Media.Imaging +{ + /// + /// Controls the way the bitmaps are drawn together. + /// + public enum BitmapBlendingMode + { + /// + /// Source is placed over the destination. + /// + SourceOver, + /// + /// Only the source will be present. + /// + Source, + /// + /// Only the destination will be present. + /// + Destination, + /// + /// Destination is placed over the source. + /// + DestinationOver, + /// + /// The source that overlaps the destination, replaces the destination. + /// + SourceIn, + /// + /// Destination which overlaps the source, replaces the source. + /// + DestinationIn, + /// + /// Source is placed, where it falls outside of the destination. + /// + SourceOut, + /// + /// Destination is placed, where it falls outside of the source. + /// + DestinationOut, + /// + /// Source which overlaps the destination, replaces the destination. + /// + SourceAtop, + /// + /// Destination which overlaps the source replaces the source. + /// + DestinationAtop, + /// + /// The non-overlapping regions of source and destination are combined. + /// + Xor, + /// + /// Display the sum of the source image and destination image. + /// + Plus, + } +} diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index d6e88a7507..39d4066e55 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -148,6 +148,17 @@ namespace Avalonia.Platform /// Pops the latest pushed geometry clip. /// void PopGeometryClip(); + + /// + /// Pushes a bitmap blending value. + /// + /// The bitmap blending mode. + void PushBitmapBlendMode(BitmapBlendingMode blendingMode); + + /// + /// Pops the latest pushed bitmap blending value. + /// + void PopBitmapBlendMode(); /// /// Adds a custom draw operation diff --git a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs index 7490ad912a..79e125c5cb 100644 --- a/src/Avalonia.Visuals/Platform/IGeometryImpl.cs +++ b/src/Avalonia.Visuals/Platform/IGeometryImpl.cs @@ -11,6 +11,12 @@ namespace Avalonia.Platform /// Gets the geometry's bounding rectangle. /// Rect Bounds { get; } + + /// + /// Gets the geometry's total length as if all its contours are placed + /// in a straight line. + /// + double ContourLength { get; } /// /// Gets the geometry's bounding rectangle with the specified pen. @@ -47,5 +53,38 @@ namespace Avalonia.Platform /// The transform. /// The cloned geometry. ITransformedGeometryImpl WithTransform(Matrix transform); + + /// + /// Attempts to get the corresponding point at the + /// specified distance + /// + /// The contour distance to get from. + /// The point in the specified distance. + /// If there's valid point at the specified distance. + bool TryGetPointAtDistance(double distance, out Point point); + + /// + /// Attempts to get the corresponding point and + /// tangent from the specified distance along the + /// contour of the geometry. + /// + /// The contour distance to get from. + /// The point in the specified distance. + /// The tangent in the specified distance. + /// If there's valid point and tangent at the specified distance. + bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent); + + /// + /// Attempts to get the corresponding path segment + /// given by the two distances specified. + /// Imagine it like snipping a part of the current + /// geometry. + /// + /// The contour distance to start snipping from. + /// The contour distance to stop snipping to. + /// If ture, the resulting snipped path will start with a BeginFigure call. + /// The resulting snipped path. + /// If the snipping operation is successful. + bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs new file mode 100644 index 0000000000..0a5c1f8db6 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/BitmapBlendModeNode.cs @@ -0,0 +1,68 @@ +using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents an bitmap blending mode push or pop. + /// + internal class BitmapBlendModeNode : IDrawOperation + { + /// + /// Initializes a new instance of the class that represents an + /// push. + /// + /// The to push. + public BitmapBlendModeNode(BitmapBlendingMode bitmapBlend) + { + BlendingMode = bitmapBlend; + } + + /// + /// Initializes a new instance of the class that represents an + /// pop. + /// + public BitmapBlendModeNode() + { + } + + /// + public Rect Bounds => Rect.Empty; + + /// + /// Gets the BitmapBlend to be pushed or null if the operation represents a pop. + /// + public BitmapBlendingMode? BlendingMode { get; } + + /// + public bool HitTest(Point p) => false; + + /// + /// Determines if this draw operation equals another. + /// + /// The opacity of the other draw operation. + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + public bool Equals(BitmapBlendingMode? blendingMode) => BlendingMode == blendingMode; + + /// + public void Render(IDrawingContextImpl context) + { + if (BlendingMode.HasValue) + { + context.PushBitmapBlendMode(BlendingMode.Value); + } + else + { + context.PopBitmapBlendMode(); + } + } + + public void Dispose() + { + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index 28f426266d..e6092574c5 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -253,6 +253,21 @@ namespace Avalonia.Rendering.SceneGraph } } + /// + public void PopBitmapBlendMode() + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(null)) + { + Add(new BitmapBlendModeNode()); + } + else + { + ++_drawOperationindex; + } + } + /// public void PopOpacity() { @@ -358,6 +373,21 @@ namespace Avalonia.Rendering.SceneGraph } } + /// + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(blendingMode)) + { + Add(new BitmapBlendModeNode(blendingMode)); + } + else + { + ++_drawOperationindex; + } + } + public readonly struct UpdateState : IDisposable { public UpdateState( diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index c9052c6ef2..d3da19d8c9 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -67,6 +67,14 @@ namespace Avalonia.Rendering.SceneGraph /// The scaling mode. /// public BitmapInterpolationMode BitmapInterpolationMode { get; } + + /// + /// The bitmap blending mode. + /// + /// + /// The blending mode. + /// + public BitmapBlendingMode BitmapBlendingMode { get; } /// /// Determines if this draw operation equals another. @@ -85,12 +93,12 @@ namespace Avalonia.Rendering.SceneGraph public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { return transform == Transform && - Equals(source.Item, Source.Item) && - source.Item.Version == SourceVersion && - opacity == Opacity && - sourceRect == SourceRect && - destRect == DestRect && - bitmapInterpolationMode == BitmapInterpolationMode; + Equals(source.Item, Source.Item) && + source.Item.Version == SourceVersion && + opacity == Opacity && + sourceRect == SourceRect && + destRect == DestRect && + bitmapInterpolationMode == BitmapInterpolationMode; } /// diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 1b9f5c67d5..79c4202be4 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -82,6 +82,15 @@ namespace Avalonia public static Vector operator *(Vector vector, double scale) => Multiply(vector, scale); + /// + /// Scales a vector. + /// + /// The vector. + /// The scaling factor. + /// The scaled vector. + public static Vector operator *(double scale, Vector vector) + => Multiply(vector, scale); + /// /// Scales a vector. /// diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs index abff763bb1..a191dc59fb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -41,10 +41,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions // Targeted InsertBefore( + new AvaloniaXamlIlResolveClassesPropertiesTransformer(), new AvaloniaXamlIlTransformInstanceAttachedProperties(), new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); InsertAfter( - new AvaloniaXamlIlAvaloniaPropertyResolver()); + new AvaloniaXamlIlAvaloniaPropertyResolver(), + new AvaloniaXamlIlReorderClassesPropertiesTransformer() + ); InsertBefore( new AvaloniaXamlIlBindingPathParser(), diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs index 0c0dcb1634..f6f47dce0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs @@ -14,8 +14,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions XamlXmlnsMappings xmlnsMappings, XamlValueConverter customValueConverter, XamlIlClrPropertyInfoEmitter clrPropertyEmitter, - XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter) - : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter) + XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter, + IXamlIdentifierGenerator identifierGenerator = null) + : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter, identifierGenerator) { ClrPropertyEmitter = clrPropertyEmitter; AccessorFactoryEmitter = accessorFactoryEmitter; diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs new file mode 100644 index 0000000000..23232dbcf3 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlClassesPropertyResolver.cs @@ -0,0 +1,97 @@ +using System.Collections.Generic; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlResolveClassesPropertiesTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstNamePropertyReference prop + && prop.TargetType is XamlAstClrTypeReference targetRef + && prop.DeclaringType is XamlAstClrTypeReference declaringRef) + { + var types = context.GetAvaloniaTypes(); + if (types.StyledElement.IsAssignableFrom(targetRef.Type) + && types.Classes.Equals(declaringRef.Type)) + { + return new XamlAstClrProperty(node, "class:" + prop.Name, types.Classes, + null) + { + Setters = { new ClassValueSetter(types, prop.Name), new ClassBindingSetter(types, prop.Name) } + }; + } + } + return node; + } + + + class ClassValueSetter : IXamlEmitablePropertySetter + { + private readonly AvaloniaXamlIlWellKnownTypes _types; + private readonly string _className; + + public ClassValueSetter(AvaloniaXamlIlWellKnownTypes types, string className) + { + _types = types; + _className = className; + Parameters = new[] { types.XamlIlTypes.Boolean }; + } + + public void Emit(IXamlILEmitter emitter) + { + using (var value = emitter.LocalsPool.GetLocal(_types.XamlIlTypes.Boolean)) + { + emitter + .Stloc(value.Local) + .EmitCall(_types.StyledElementClassesProperty.Getter) + .Ldstr(_className) + .Ldloc(value.Local) + .EmitCall(_types.Classes.GetMethod(new FindMethodMethodSignature("Set", + _types.XamlIlTypes.Void, _types.XamlIlTypes.String, _types.XamlIlTypes.Boolean))); + } + } + + public IXamlType TargetType => _types.StyledElement; + + public PropertySetterBinderParameters BinderParameters { get; } = + new PropertySetterBinderParameters { AllowXNull = false }; + public IReadOnlyList Parameters { get; } + } + + class ClassBindingSetter : IXamlEmitablePropertySetter + { + private readonly AvaloniaXamlIlWellKnownTypes _types; + private readonly string _className; + + public ClassBindingSetter(AvaloniaXamlIlWellKnownTypes types, string className) + { + _types = types; + _className = className; + Parameters = new[] {types.IBinding}; + } + + public void Emit(IXamlILEmitter emitter) + { + using (var bloc = emitter.LocalsPool.GetLocal(_types.IBinding)) + emitter + .Stloc(bloc.Local) + .Ldstr(_className) + .Ldloc(bloc.Local) + // TODO: provide anchor? + .Ldnull(); + emitter.EmitCall(_types.ClassesBindMethod, true); + } + + public IXamlType TargetType => _types.StyledElement; + + public PropertySetterBinderParameters BinderParameters { get; } = + new PropertySetterBinderParameters { AllowXNull = false }; + public IReadOnlyList Parameters { get; } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs new file mode 100644 index 0000000000..ae3515a6d6 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlReorderClassesPropertiesTransformer.cs @@ -0,0 +1,40 @@ +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlReorderClassesPropertiesTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstObjectNode obj) + { + IXamlAstNode classesNode = null; + IXamlAstNode firstSingleClassNode = null; + var types = context.GetAvaloniaTypes(); + foreach (var child in obj.Children) + { + if (child is XamlAstXamlPropertyValueNode propValue + && propValue.Property is XamlAstClrProperty prop) + { + if (prop.DeclaringType.Equals(types.Classes)) + { + if (firstSingleClassNode == null) + firstSingleClassNode = child; + } + else if (prop.Name == "Classes" && prop.DeclaringType.Equals(types.StyledElement)) + classesNode = child; + } + } + + if (classesNode != null && firstSingleClassNode != null) + { + obj.Children.Remove(classesNode); + obj.Children.Insert(obj.Children.IndexOf(firstSingleClassNode), classesNode); + } + } + + return node; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 34aae2c5ed..c4995b2de3 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -25,6 +25,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType AssignBindingAttribute { get; } public IXamlType UnsetValueType { get; } public IXamlType StyledElement { get; } + public IXamlType IStyledElement { get; } public IXamlType NameScope { get; } public IXamlMethod NameScopeSetNameScope { get; } public IXamlType INameScope { get; } @@ -78,6 +79,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlType ColumnDefinition { get; } public IXamlType ColumnDefinitions { get; } public IXamlType Classes { get; } + public IXamlMethod ClassesBindMethod { get; } + public IXamlProperty StyledElementClassesProperty { get; set; } public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { @@ -97,6 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers IBinding, cfg.WellKnownTypes.Object); UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType"); StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement"); + IStyledElement = cfg.TypeSystem.GetType("Avalonia.IStyledElement"); INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope"); INameScopeRegister = INameScope.GetMethod( new FindMethodMethodSignature("Register", XamlIlTypes.Void, @@ -168,6 +172,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers RowDefinition = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinition"); RowDefinitions = cfg.TypeSystem.GetType("Avalonia.Controls.RowDefinitions"); Classes = cfg.TypeSystem.GetType("Avalonia.Controls.Classes"); + StyledElementClassesProperty = + StyledElement.Properties.First(x => x.Name == "Classes" && x.PropertyType.Equals(Classes)); + ClassesBindMethod = cfg.TypeSystem.GetType("Avalonia.StyledElementExtensions") + .FindMethod( "BindClass", IDisposable, false, IStyledElement, + cfg.WellKnownTypes.String, + IBinding, cfg.WellKnownTypes.Object); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 07c5451135..650534b347 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -30,7 +30,7 @@ namespace Avalonia.Markup.Xaml.Templates public IControl Build(object data, IControl existing) { - return existing ?? TemplateContent.Load(Content).Control; + return existing ?? TemplateContent.Load(Content)?.Control; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index c8843a3176..c096ed7ed7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -10,8 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object Content { get; set; } - public IPanel Build() - => (IPanel)TemplateContent.Load(Content).Control; + public IPanel Build() => (IPanel)TemplateContent.Load(Content)?.Control; object ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs index 65323ae665..45fae9cb28 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs @@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object Content { get; set; } - public IControl Build() => TemplateContent.Load(Content).Control; + public IControl Build() => TemplateContent.Load(Content)?.Control; object ITemplate.Build() => Build(); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index 96f25668fb..483a1a5d06 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -1,6 +1,4 @@ using System; -using Avalonia.Controls; -using System.Collections.Generic; using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml.Templates @@ -14,6 +12,12 @@ namespace Avalonia.Markup.Xaml.Templates { return (ControlTemplateResult)direct(null); } + + if (templateContent is null) + { + return null; + } + throw new ArgumentException(nameof(templateContent)); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index d785ac4ac0..7b065c7f47 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -51,8 +51,12 @@ namespace Avalonia.Markup.Xaml.Templates public IControl Build(object data) { - var visualTreeForItem = TemplateContent.Load(Content).Control; - visualTreeForItem.DataContext = data; + var visualTreeForItem = TemplateContent.Load(Content)?.Control; + if (visualTreeForItem != null) + { + visualTreeForItem.DataContext = data; + } + return visualTreeForItem; } } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 2a79a4bb50..b7d5d3ec59 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -23,9 +23,11 @@ namespace Avalonia.Skia private readonly Vector _dpi; private readonly Stack _maskStack = new Stack(); private readonly Stack _opacityStack = new Stack(); + private readonly Stack _blendingModeStack = new Stack(); private readonly Matrix? _postTransform; private readonly IVisualBrushRenderer _visualBrushRenderer; private double _currentOpacity = 1.0f; + private BitmapBlendingMode _currentBlendingMode = BitmapBlendingMode.SourceOver; private readonly bool _canTextUseLcdRendering; private Matrix _currentTransform; private bool _disposed; @@ -145,6 +147,7 @@ namespace Avalonia.Skia }) { paint.FilterQuality = bitmapInterpolationMode.ToSKFilterQuality(); + paint.BlendMode = _currentBlendingMode.ToSKBlendMode(); drawableImage.Draw(this, s, d, paint); } @@ -508,6 +511,19 @@ namespace Avalonia.Skia Canvas.Restore(); } + /// + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + _blendingModeStack.Push(_currentBlendingMode); + _currentBlendingMode = blendingMode; + } + + /// + public void PopBitmapBlendMode() + { + _currentBlendingMode = _blendingModeStack.Pop(); + } + public void Custom(ICustomDrawOperation custom) => custom.Render(this); /// diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index 879b18742e..19dbcf9713 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -11,9 +11,36 @@ namespace Avalonia.Skia internal abstract class GeometryImpl : IGeometryImpl { private PathCache _pathCache; - + private SKPathMeasure _pathMeasureCache; + + private SKPathMeasure CachedPathMeasure + { + get + { + if (_pathMeasureCache is null) + { + _pathMeasureCache = new SKPathMeasure(EffectivePath); + } + + return _pathMeasureCache; + } + } + /// public abstract Rect Bounds { get; } + + /// + public double ContourLength + { + get + { + if (EffectivePath is null) + return 0; + + return (double)CachedPathMeasure?.Length; + } + } + public abstract SKPath EffectivePath { get; } /// @@ -30,12 +57,12 @@ namespace Avalonia.Skia // Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic. var strokeWidth = (float)(pen?.Thickness ?? 0); - + if (!_pathCache.HasCacheFor(strokeWidth)) { UpdatePathCache(strokeWidth); } - + return PathContainsCore(_pathCache.CachedStrokePath, point); } @@ -58,7 +85,7 @@ namespace Avalonia.Skia { paint.IsStroke = true; paint.StrokeWidth = strokeWidth; - + paint.GetFillPath(EffectivePath, strokePath); _pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect()); @@ -74,13 +101,13 @@ namespace Avalonia.Skia /// True, if point is contained in a path. private static bool PathContainsCore(SKPath path, Point point) { - return path.Contains((float)point.X, (float)point.Y); + return path.Contains((float)point.X, (float)point.Y); } /// public IGeometryImpl Intersect(IGeometryImpl geometry) { - var result = EffectivePath.Op(((GeometryImpl) geometry).EffectivePath, SKPathOp.Intersect); + var result = EffectivePath.Op(((GeometryImpl)geometry).EffectivePath, SKPathOp.Intersect); return result == null ? null : new StreamGeometryImpl(result); } @@ -89,21 +116,74 @@ namespace Avalonia.Skia public Rect GetRenderBounds(IPen pen) { var strokeWidth = (float)(pen?.Thickness ?? 0); - + if (!_pathCache.HasCacheFor(strokeWidth)) { UpdatePathCache(strokeWidth); } - + return _pathCache.CachedGeometryRenderBounds; } - + /// public ITransformedGeometryImpl WithTransform(Matrix transform) { return new TransformedGeometryImpl(this, transform); } + /// + public bool TryGetPointAtDistance(double distance, out Point point) + { + if (EffectivePath is null) + { + point = new Point(); + return false; + } + + var res = CachedPathMeasure.GetPosition((float)distance, out var skPoint); + point = new Point(skPoint.X, skPoint.Y); + return res; + } + + /// + public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) + { + if (EffectivePath is null) + { + point = new Point(); + tangent = new Point(); + return false; + } + + var res = CachedPathMeasure.GetPositionAndTangent((float)distance, out var skPoint, out var skTangent); + point = new Point(skPoint.X, skPoint.Y); + tangent = new Point(skTangent.X, skTangent.Y); + return res; + } + + public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, + out IGeometryImpl segmentGeometry) + { + if (EffectivePath is null) + { + segmentGeometry = null; + return false; + } + + segmentGeometry = null; + + var _skPathSegment = new SKPath(); + + var res = CachedPathMeasure.GetSegment((float)startDistance, (float)stopDistance, _skPathSegment, startOnBeginFigure); + + if (res) + { + segmentGeometry = new StreamGeometryImpl(_skPathSegment); + } + + return res; + } + /// /// Invalidate all caches. Call after chaining path contents. /// @@ -115,12 +195,12 @@ namespace Avalonia.Skia private struct PathCache { private float _cachedStrokeWidth; - + /// /// Tolerance for two stroke widths to be deemed equal /// public const float Tolerance = float.Epsilon; - + /// /// Cached contour path. /// diff --git a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs index bb3dbbfadc..75b4231640 100644 --- a/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs +++ b/src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs @@ -25,6 +25,39 @@ namespace Avalonia.Skia } } + public static SKBlendMode ToSKBlendMode(this BitmapBlendingMode blendingMode) + { + switch (blendingMode) + { + case BitmapBlendingMode.SourceOver: + return SKBlendMode.SrcOver; + case BitmapBlendingMode.Source: + return SKBlendMode.Src; + case BitmapBlendingMode.SourceIn: + return SKBlendMode.SrcIn; + case BitmapBlendingMode.SourceOut: + return SKBlendMode.SrcOut; + case BitmapBlendingMode.SourceAtop: + return SKBlendMode.SrcATop; + case BitmapBlendingMode.Destination: + return SKBlendMode.Dst; + case BitmapBlendingMode.DestinationIn: + return SKBlendMode.DstIn; + case BitmapBlendingMode.DestinationOut: + return SKBlendMode.DstOut; + case BitmapBlendingMode.DestinationOver: + return SKBlendMode.DstOver; + case BitmapBlendingMode.DestinationAtop: + return SKBlendMode.DstATop; + case BitmapBlendingMode.Xor: + return SKBlendMode.Xor; + case BitmapBlendingMode.Plus: + return SKBlendMode.Plus; + default: + throw new ArgumentOutOfRangeException(nameof(blendingMode), blendingMode, null); + } + } + public static SKPoint ToSKPoint(this Point p) { return new SKPoint((float)p.X, (float)p.Y); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 47a19aad8c..af35934785 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -5,6 +5,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; using SharpDX; using SharpDX.Direct2D1; using SharpDX.Mathematics.Interop; @@ -121,7 +122,9 @@ namespace Avalonia.Direct2D1.Media using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) { var interpolationMode = GetInterpolationMode(bitmapInterpolationMode); - + + // TODO: How to implement CompositeMode here? + _deviceContext.DrawBitmap( d2d.Value, destRect.ToSharpDX(), @@ -149,6 +152,35 @@ namespace Avalonia.Direct2D1.Media } } + public static CompositeMode GetCompositeMode(BitmapBlendingMode blendingMode) + { + switch (blendingMode) + { + case BitmapBlendingMode.SourceIn: + return CompositeMode.SourceIn; + case BitmapBlendingMode.SourceOut: + return CompositeMode.SourceOut; + case BitmapBlendingMode.SourceOver: + return CompositeMode.SourceOver; + case BitmapBlendingMode.SourceAtop: + return CompositeMode.SourceAtop; + case BitmapBlendingMode.DestinationIn: + return CompositeMode.DestinationIn; + case BitmapBlendingMode.DestinationOut: + return CompositeMode.DestinationOut; + case BitmapBlendingMode.DestinationOver: + return CompositeMode.DestinationOver; + case BitmapBlendingMode.DestinationAtop: + return CompositeMode.DestinationAtop; + case BitmapBlendingMode.Xor: + return CompositeMode.Xor; + case BitmapBlendingMode.Plus: + return CompositeMode.Plus; + default: + throw new ArgumentOutOfRangeException(nameof(blendingMode), blendingMode, null); + } + } + /// /// Draws a bitmap image. /// @@ -525,6 +557,16 @@ namespace Avalonia.Direct2D1.Media PopLayer(); } + public void PushBitmapBlendMode(BitmapBlendingMode blendingMode) + { + // TODO: Stubs for now + } + + public void PopBitmapBlendMode() + { + // TODO: Stubs for now + } + public void PushOpacityMask(IBrush mask, Rect bounds) { var parameters = new LayerParameters diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs index d04e2b3110..ec88347a17 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs @@ -1,3 +1,4 @@ +using Avalonia.Logging; using Avalonia.Platform; using SharpDX.Direct2D1; @@ -8,6 +9,8 @@ namespace Avalonia.Direct2D1.Media /// public abstract class GeometryImpl : IGeometryImpl { + private const float ContourApproximation = 0.0001f; + public GeometryImpl(Geometry geometry) { Geometry = geometry; @@ -16,6 +19,9 @@ namespace Avalonia.Direct2D1.Media /// public Rect Bounds => Geometry.GetWidenedBounds(0).ToAvalonia(); + /// + public double ContourLength => Geometry.ComputeLength(null, ContourApproximation); + public Geometry Geometry { get; } /// @@ -57,6 +63,33 @@ namespace Avalonia.Direct2D1.Media transform.ToDirect2D()), this); } + + /// + public bool TryGetPointAtDistance(double distance, out Point point) + { + Geometry.ComputePointAtLength((float)distance, ContourApproximation, out var tangentVector); + point = new Point(tangentVector.X, tangentVector.Y); + return true; + } + + /// + public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) + { + // Direct2D doesnt have this sadly. + Logger.TryGet(LogEventLevel.Warning, LogArea.Visual)?.Log(this, "TryGetPointAndTangentAtDistance is not available in Direct2D."); + point = new Point(); + tangent = new Point(); + return false; + } + + public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, out IGeometryImpl segmentGeometry) + { + // Direct2D doesnt have this too sadly. + Logger.TryGet(LogEventLevel.Warning, LogArea.Visual)?.Log(this, "TryGetSegment is not available in Direct2D."); + + segmentGeometry = null; + return false; + } protected virtual Geometry GetSourceGeometry() => Geometry; } diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index a2b1681c5c..082aca1109 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -85,6 +85,7 @@ namespace Avalonia.Win32 private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; private bool _isCloseRequested; private bool _shown; + private bool _hiddenWindowIsParent; public WindowImpl() { @@ -483,8 +484,8 @@ namespace Avalonia.Win32 IntPtr.Zero, 0, 0, - requestedClientWidth + (windowRect.Width - clientRect.Width), - requestedClientHeight + (windowRect.Height - clientRect.Height), + requestedClientWidth + (_isClientAreaExtended ? 0 : windowRect.Width - clientRect.Width), + requestedClientHeight + (_isClientAreaExtended ? 0 : windowRect.Height - clientRect.Height), SetWindowPosFlags.SWP_RESIZE); } } @@ -571,8 +572,7 @@ namespace Avalonia.Win32 public virtual void Show(bool activate) { - SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent != null ? _parent._hwnd : IntPtr.Zero); - + SetParent(_parent); ShowWindow(_showWindowState, activate); } @@ -581,7 +581,16 @@ namespace Avalonia.Win32 public void SetParent(IWindowImpl parent) { _parent = (WindowImpl)parent; - SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, _parent._hwnd); + + var parentHwnd = _parent?._hwnd ?? IntPtr.Zero; + + if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar) + { + parentHwnd = OffscreenParentWindow.Handle; + _hiddenWindowIsParent = true; + } + + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd); } public void SetEnabled(bool enable) => EnableWindow(_hwnd, enable); @@ -883,20 +892,19 @@ namespace Avalonia.Win32 _isClientAreaExtended = false; return; } - - GetWindowRect(_hwnd, out var rcClient); + GetClientRect(_hwnd, out var rcClient); + GetWindowRect(_hwnd, out var rcWindow); // Inform the application of the frame change. SetWindowPos(_hwnd, - IntPtr.Zero, - rcClient.left, rcClient.top, - rcClient.Width, rcClient.Height, - SetWindowPosFlags.SWP_FRAMECHANGED); + IntPtr.Zero, + rcWindow.left, rcWindow.top, + rcClient.Width, rcClient.Height, + SetWindowPosFlags.SWP_FRAMECHANGED); if (_isClientAreaExtended && WindowState != WindowState.FullScreen) { var margins = UpdateExtendMargins(); - DwmExtendFrameIntoClientArea(_hwnd, ref margins); } else @@ -906,6 +914,8 @@ namespace Avalonia.Win32 _offScreenMargin = new Thickness(); _extendedMargins = new Thickness(); + + Resize(new Size(rcWindow.Width/ RenderScaling, rcWindow.Height / RenderScaling)); } if(!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) && @@ -1094,16 +1104,38 @@ namespace Avalonia.Win32 if (newProperties.ShowInTaskbar) { exStyle |= WindowStyles.WS_EX_APPWINDOW; + + if (_hiddenWindowIsParent) + { + // Can't enable the taskbar icon by clearing the parent window unless the window + // is hidden. Hide the window and show it again with the same activation state + // when we've finished. Interestingly it seems to work fine the other way. + var shown = IsWindowVisible(_hwnd); + var activated = GetActiveWindow() == _hwnd; + + if (shown) + Hide(); + + _hiddenWindowIsParent = false; + SetParent(null); + + if (shown) + Show(activated); + } } else { + // To hide a non-owned window's taskbar icon we need to parent it to a hidden window. + if (_parent is null) + { + SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, OffscreenParentWindow.Handle); + _hiddenWindowIsParent = true; + } + exStyle &= ~WindowStyles.WS_EX_APPWINDOW; } SetExtendedStyle(exStyle); - - // TODO: To hide non-owned window from taskbar we need to parent it to a hidden window. - // Otherwise it will still show in the taskbar. } WindowStyles style; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 4f2b580bce..8af638c5d7 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -377,5 +377,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml public string Greeting1 { get; set; } = "Hello"; public string Greeting2 { get; set; } = "World"; } + + [Fact] + public void Binding_Classes_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + // Note, this test also checks `Classes` reordering, so it should be kept AFTER the last single class + var xaml = @" + +