From 7b6cabe6085409ad0d86bbe4bf777663c7c557ca Mon Sep 17 00:00:00 2001 From: affederaffe <68356204+affederaffe@users.noreply.github.com> Date: Tue, 28 Mar 2023 17:24:52 +0200 Subject: [PATCH 1/9] Fix TrayIcons on Gnome --- .../Avalonia.FreeDesktop.csproj | 10 ++++----- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 21 ++++++++++--------- src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs | 13 ++++++++++-- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index da9add1fa4..da6ae9e856 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -5,20 +5,20 @@ enable - + - + - + - - + + diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 3073ea580c..77f9d31273 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -38,7 +38,7 @@ namespace Avalonia.FreeDesktop private bool _resetQueued; private int _nextId = 1; - public DBusMenuExporterImpl(Connection connection, IntPtr xid) + public DBusMenuExporterImpl(Connection connection, IntPtr xid) : this() { Connection = connection; _xid = (uint)xid.ToInt32(); @@ -47,7 +47,7 @@ namespace Avalonia.FreeDesktop _ = InitializeAsync(); } - public DBusMenuExporterImpl(Connection connection, string path) + public DBusMenuExporterImpl(Connection connection, string path) : this() { Connection = connection; _appMenu = false; @@ -56,6 +56,13 @@ namespace Avalonia.FreeDesktop _ = InitializeAsync(); } + private DBusMenuExporterImpl() + { + BackingProperties.Status = string.Empty; + BackingProperties.TextDirection = string.Empty; + BackingProperties.IconThemePath = Array.Empty(); + } + protected override Connection Connection { get; } public override string Path { get; } @@ -202,15 +209,9 @@ namespace Avalonia.FreeDesktop return id; } - private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) - { - QueueReset(); - } + private void OnMenuItemsChanged(object? sender, NotifyCollectionChangedEventArgs e) => QueueReset(); - private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) - { - QueueReset(); - } + private void OnItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) => QueueReset(); private static readonly string[] s_allProperties = { "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" diff --git a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs index fed8b87bc9..43ae48341c 100644 --- a/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs +++ b/src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs @@ -220,6 +220,16 @@ namespace Avalonia.FreeDesktop { Connection = connection; BackingProperties.Menu = dbusMenuPath; + BackingProperties.Category = string.Empty; + BackingProperties.Status = string.Empty; + BackingProperties.Id = string.Empty; + BackingProperties.Title = string.Empty; + BackingProperties.IconPixmap = Array.Empty<(int, int, byte[])>(); + BackingProperties.AttentionIconName = string.Empty; + BackingProperties.AttentionIconPixmap = Array.Empty<(int, int, byte[])>(); + BackingProperties.AttentionMovieName = string.Empty; + BackingProperties.OverlayIconName = string.Empty; + BackingProperties.OverlayIconPixmap = Array.Empty<(int, int, byte[])>(); BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), string.Empty, string.Empty); InvalidateAll(); } @@ -234,7 +244,7 @@ namespace Avalonia.FreeDesktop protected override ValueTask OnActivateAsync(int x, int y) { - Dispatcher.UIThread.Post(() => ActivationDelegate?.Invoke()); + ActivationDelegate?.Invoke(); return new ValueTask(); } @@ -267,7 +277,6 @@ namespace Avalonia.FreeDesktop BackingProperties.Category = "ApplicationStatus"; BackingProperties.Status = text; BackingProperties.Title = text; - BackingProperties.ToolTip = (string.Empty, Array.Empty<(int, int, byte[])>(), text, string.Empty); InvalidateAll(); } } From b2a4e85e23b78ec031be0bb6b12fffc1b8cd5528 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 29 Mar 2023 14:20:46 +0200 Subject: [PATCH 2/9] Only bypass WM_CHAR if previous composition is not empty --- src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 39bdb942e1..84f27f080e 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -729,7 +729,7 @@ namespace Avalonia.Win32 { case GCS.GCS_RESULTSTR: { - if(ToInt32(wParam) >= 32) + if(!string.IsNullOrEmpty(previousComposition) && ToInt32(wParam) >= 32) { Imm32InputMethod.Current.Composition = previousComposition; From 7bb6d06ac50f393c667715eb3393a36b282b78e4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 15:24:29 +0200 Subject: [PATCH 3/9] Added failing test for #10626 and #10718. --- .../MenuItemTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 1049ff2678..909b65853c 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Text; using System.Windows.Input; using Avalonia.Collections; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; using Avalonia.Platform; @@ -348,6 +350,46 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Menu_ItemTemplate_Should_Be_Applied_To_TopLevel_MenuItem_Header() + { + using var app = Application(); + + var items = new[] + { + new MenuViewModel("Foo"), + new MenuViewModel("Bar"), + }; + + var itemTemplate = new FuncDataTemplate((x, _) => + new TextBlock { Text = x.Header }); + + var menu = new Menu + { + ItemTemplate = itemTemplate, + ItemsSource = items, + }; + + var window = new Window { Content = menu }; + window.LayoutManager.ExecuteInitialLayoutPass(); + + var panel = Assert.IsType(menu.Presenter.Panel); + Assert.Equal(2, panel.Children.Count); + + for (var i = 0; i < panel.Children.Count; i++) + { + var menuItem = Assert.IsType(panel.Children[i]); + + Assert.Equal(items[i], menuItem.Header); + + var headerPresenter = Assert.IsType(menuItem.HeaderPresenter); + Assert.Same(itemTemplate, headerPresenter.ContentTemplate); + + var headerControl = Assert.IsType(headerPresenter.Child); + Assert.Equal(items[i].Header, headerControl.Text); + } + } + private IDisposable Application() { var screen = new PixelRect(new PixelPoint(), new PixelSize(100, 100)); @@ -401,5 +443,7 @@ namespace Avalonia.Controls.UnitTests public void RaiseCanExecuteChanged() => _canExecuteChanged?.Invoke(this, EventArgs.Empty); } + + private record MenuViewModel(string Header); } } From b7a249107bb9475b1aba7a942418a6fcd2d0a30e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 29 Mar 2023 15:33:43 +0200 Subject: [PATCH 4/9] Make data templates work again with MenuItem. - `MenuItem` is a `HeaderedSelectingItemsControl` not a `HeaderedItemsControl` so need to separate logic for that case when preparing items - Added `HeaderTemplate` to `HeaderedSelectingItemsControl ` - Tweaked logic for selecting header templates: parent's `ItemTemplate` should be used if set (cross-checked with WPF) - Update menu templates to bind to menu item's `HeaderTemplate` Fixes #10626 Fixes #10718 --- src/Avalonia.Controls/ItemsControl.cs | 12 +++- .../Primitives/HeaderedItemsControl.cs | 16 ++--- .../HeaderedSelectingItemsControl.cs | 63 +++++++++++++++++++ src/Avalonia.Themes.Fluent/Controls/Menu.xaml | 1 + .../Controls/MenuItem.xaml | 2 +- src/Avalonia.Themes.Simple/Controls/Menu.xaml | 3 +- .../Controls/MenuItem.xaml | 2 +- .../MenuItemTests.cs | 1 + 8 files changed, 86 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 1123f42afa..06f0d54e4c 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -460,13 +460,19 @@ namespace Avalonia.Controls ic.ItemContainerTheme = ict; } - // This condition is separate because HeaderedItemsControl needs to also run the - // ItemsControl preparation. + // These conditions are separate because HeaderedItemsControl and + // HeaderedSelectingItemsControl also need to run the ItemsControl preparation. if (container is HeaderedItemsControl hic) { hic.Header = item; hic.HeaderTemplate = itemTemplate; - hic.PrepareItemContainer(); + hic.PrepareItemContainer(this); + } + else if (container is HeaderedSelectingItemsControl hsic) + { + hsic.Header = item; + hsic.HeaderTemplate = itemTemplate; + hsic.PrepareItemContainer(this); } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index 55d2ec7506..273271d2ce 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.Primitives public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { private IDisposable? _itemsBinding; - private bool _prepareItemContainerOnAttach; + private ItemsControl? _prepareItemContainerOnAttach; /// /// Defines the property. @@ -69,10 +69,10 @@ namespace Avalonia.Controls.Primitives { base.OnAttachedToLogicalTree(e); - if (_prepareItemContainerOnAttach) + if (_prepareItemContainerOnAttach is not null) { - PrepareItemContainer(); - _prepareItemContainerOnAttach = false; + PrepareItemContainer(_prepareItemContainerOnAttach); + _prepareItemContainerOnAttach = null; } } @@ -97,7 +97,7 @@ namespace Avalonia.Controls.Primitives return false; } - internal void PrepareItemContainer() + internal void PrepareItemContainer(ItemsControl parent) { _itemsBinding?.Dispose(); _itemsBinding = null; @@ -106,18 +106,18 @@ namespace Avalonia.Controls.Primitives if (item is null) { - _prepareItemContainerOnAttach = false; + _prepareItemContainerOnAttach = null; return; } - var headerTemplate = HeaderTemplate; + var headerTemplate = HeaderTemplate ?? parent.ItemTemplate; if (headerTemplate is null) { if (((ILogical)this).IsAttachedToLogicalTree) headerTemplate = this.FindDataTemplate(item); else - _prepareItemContainerOnAttach = true; + _prepareItemContainerOnAttach = parent; } if (headerTemplate is ITreeDataTemplate treeTemplate && diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index 49fc58c8f5..88ca1f1fe1 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -1,5 +1,8 @@ +using System; using Avalonia.Collections; using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives @@ -9,12 +12,21 @@ namespace Avalonia.Controls.Primitives /// public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost { + private IDisposable? _itemsBinding; + private ItemsControl? _prepareItemContainerOnAttach; + /// /// Defines the property. /// public static readonly StyledProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty HeaderTemplateProperty = + HeaderedItemsControl.HeaderTemplateProperty.AddOwner(); + /// /// Initializes static members of the class. /// @@ -32,6 +44,15 @@ namespace Avalonia.Controls.Primitives set { SetValue(HeaderProperty, value); } } + /// + /// Gets or sets the data template used to display the header content of the control. + /// + public IDataTemplate? HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + /// /// Gets the header presenter from the control's template. /// @@ -50,6 +71,17 @@ namespace Avalonia.Controls.Primitives return RegisterContentPresenter(presenter); } + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + base.OnAttachedToLogicalTree(e); + + if (_prepareItemContainerOnAttach is not null) + { + PrepareItemContainer(_prepareItemContainerOnAttach); + _prepareItemContainerOnAttach = null; + } + } + /// /// Called when an is registered with the control. /// @@ -65,6 +97,37 @@ namespace Avalonia.Controls.Primitives return false; } + internal void PrepareItemContainer(ItemsControl parent) + { + _itemsBinding?.Dispose(); + _itemsBinding = null; + + var item = Header; + + if (item is null) + { + _prepareItemContainerOnAttach = null; + return; + } + + var headerTemplate = HeaderTemplate ?? parent.ItemTemplate; + + if (headerTemplate is null) + { + if (((ILogical)this).IsAttachedToLogicalTree) + headerTemplate = this.FindDataTemplate(item); + else + _prepareItemContainerOnAttach = parent; + } + + if (headerTemplate is ITreeDataTemplate treeTemplate && + treeTemplate.Match(item) && + treeTemplate.ItemsSelector(item) is { } itemsBinding) + { + _itemsBinding = BindingOperations.Apply(this, ItemsSourceProperty, itemsBinding, null); + } + } + private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) { if (e.OldValue is ILogical oldChild) diff --git a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml index c234cfd68e..e6bbbde632 100644 --- a/src/Avalonia.Themes.Fluent/Controls/Menu.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/Menu.xaml @@ -28,6 +28,7 @@ + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding HeaderTemplate}"> diff --git a/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml b/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml index 2f09b9dc40..59ddcdf325 100644 --- a/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Simple/Controls/MenuItem.xaml @@ -43,7 +43,7 @@ Margin="{TemplateBinding Padding}" VerticalAlignment="Center" Content="{TemplateBinding Header}" - ContentTemplate="{TemplateBinding ItemTemplate}"> + ContentTemplate="{TemplateBinding HeaderTemplate}"> diff --git a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs index 909b65853c..6fda5209ad 100644 --- a/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MenuItemTests.cs @@ -381,6 +381,7 @@ namespace Avalonia.Controls.UnitTests var menuItem = Assert.IsType(panel.Children[i]); Assert.Equal(items[i], menuItem.Header); + Assert.Same(itemTemplate, menuItem.HeaderTemplate); var headerPresenter = Assert.IsType(menuItem.HeaderPresenter); Assert.Same(itemTemplate, headerPresenter.ContentTemplate); From 312f1250e27a08b01e2a1a594531e560fab5820c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 30 Mar 2023 00:25:18 +0600 Subject: [PATCH 5/9] Don't use AvaloniaObject in IPlatformRenderInterface --- src/Avalonia.Base/CombinedGeometry.cs | 14 +++++--------- src/Avalonia.Base/Media/GeometryGroup.cs | 5 ++++- .../Platform/IPlatformRenderInterface.cs | 4 ++-- .../HeadlessPlatformRenderInterface.cs | 4 ++-- src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs | 7 ++++--- src/Skia/Avalonia.Skia/GeometryGroupImpl.cs | 7 ++++--- src/Skia/Avalonia.Skia/PlatformRenderInterface.cs | 4 ++-- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 4 ++-- .../Media/CombinedGeometryImpl.cs | 13 +++++++------ .../Avalonia.Direct2D1/Media/GeometryGroupImpl.cs | 7 ++++--- .../VisualTree/MockRenderInterface.cs | 4 ++-- tests/Avalonia.Benchmarks/NullRenderingPlatform.cs | 4 ++-- .../MockPlatformRenderInterface.cs | 4 ++-- 13 files changed, 42 insertions(+), 39 deletions(-) diff --git a/src/Avalonia.Base/CombinedGeometry.cs b/src/Avalonia.Base/CombinedGeometry.cs index 8f080d05c7..4b5866519b 100644 --- a/src/Avalonia.Base/CombinedGeometry.cs +++ b/src/Avalonia.Base/CombinedGeometry.cs @@ -152,19 +152,15 @@ namespace Avalonia.Media var g1 = Geometry1; var g2 = Geometry2; - if (g1 is object && g2 is object) + if (g1?.PlatformImpl != null && g2?.PlatformImpl != null) { var factory = AvaloniaLocator.Current.GetRequiredService(); - return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2); + return factory.CreateCombinedGeometry(GeometryCombineMode, g1.PlatformImpl, g2.PlatformImpl); } - else if (GeometryCombineMode == GeometryCombineMode.Intersect) - return null; - else if (g1 is object) - return g1.PlatformImpl; - else if (g2 is object) - return g2.PlatformImpl; - else + + if (GeometryCombineMode == GeometryCombineMode.Intersect) return null; + return g1?.PlatformImpl ?? g2?.PlatformImpl; } } } diff --git a/src/Avalonia.Base/Media/GeometryGroup.cs b/src/Avalonia.Base/Media/GeometryGroup.cs index 0326e606f4..3e61413919 100644 --- a/src/Avalonia.Base/Media/GeometryGroup.cs +++ b/src/Avalonia.Base/Media/GeometryGroup.cs @@ -78,7 +78,10 @@ namespace Avalonia.Media { var factory = AvaloniaLocator.Current.GetRequiredService(); - return factory.CreateGeometryGroup(FillRule, _children); + var children = new IGeometryImpl?[_children.Count]; + for (var c = 0; c < _children.Count; c++) + children[c] = _children[c].PlatformImpl; + return factory.CreateGeometryGroup(FillRule, children!); } return null; diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index cfc7fac3ea..81fe2c046f 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -48,7 +48,7 @@ namespace Avalonia.Platform /// The fill rule. /// The geometries to group. /// A combined geometry. - IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); + IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children); /// /// Creates a geometry group implementation. @@ -57,7 +57,7 @@ namespace Avalonia.Platform /// The first geometry. /// The second geometry. /// A combined geometry. - IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2); + IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2); /// /// Created a geometry implementation for the glyph run. diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index f8100d3832..431989134a 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -47,8 +47,8 @@ namespace Avalonia.Headless } public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub(); - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => throw new NotImplementedException(); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs index 1a4f467f40..1e9240ec70 100644 --- a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs @@ -1,4 +1,5 @@ using Avalonia.Media; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia @@ -15,10 +16,10 @@ namespace Avalonia.Skia Bounds = (stroke ?? fill)?.TightBounds.ToAvaloniaRect() ?? default; } - public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { - if (g1.PlatformImpl is GeometryImpl i1 - && g2.PlatformImpl is GeometryImpl i2 + if (g1 is GeometryImpl i1 + && g2 is GeometryImpl i2 && TryCreate(combineMode, i1, i2) is { } result) return result; diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs index 01be42bad0..200247095f 100644 --- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Platform; using SkiaSharp; namespace Avalonia.Skia @@ -9,7 +10,7 @@ namespace Avalonia.Skia /// internal class GeometryGroupImpl : GeometryImpl { - public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) + public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children) { var fillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd; var count = children.Count; @@ -22,7 +23,7 @@ namespace Avalonia.Skia bool requiresFillPass = false; for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl geo) + if (children[i] is GeometryImpl geo) { if (geo.StrokePath != null) stroke.AddPath(geo.StrokePath); @@ -42,7 +43,7 @@ namespace Avalonia.Skia for (var i = 0; i < count; ++i) { - if (children[i].PlatformImpl is GeometryImpl { FillPath: { } fillPath }) + if (children[i] is GeometryImpl { FillPath: { } fillPath }) fill.AddPath(fillPath); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 9c4b326f14..a9a79ff0c5 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -58,12 +58,12 @@ namespace Avalonia.Skia return new StreamGeometryImpl(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { return new GeometryGroupImpl(fillRule, children); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { return CombinedGeometryImpl.ForceCreate(combineMode, g1, g2); } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 287db92b4d..826296b055 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -158,8 +158,8 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2); public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect); public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl(); - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => new CombinedGeometryImpl(combineMode, g1, g2); public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos, Point baselineOrigin) diff --git a/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs index 5a13c10bbc..b1ce160707 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs @@ -1,3 +1,4 @@ +using Avalonia.Platform; using SharpDX.Direct2D1; using AM = Avalonia.Media; @@ -13,19 +14,19 @@ namespace Avalonia.Direct2D1.Media /// public CombinedGeometryImpl( AM.GeometryCombineMode combineMode, - AM.Geometry geometry1, - AM.Geometry geometry2) + IGeometryImpl geometry1, + IGeometryImpl geometry2) : base(CreateGeometry(combineMode, geometry1, geometry2)) { } private static Geometry CreateGeometry( AM.GeometryCombineMode combineMode, - AM.Geometry geometry1, - AM.Geometry geometry2) + IGeometryImpl geometry1, + IGeometryImpl geometry2) { - var g1 = ((GeometryImpl)geometry1.PlatformImpl).Geometry; - var g2 = ((GeometryImpl)geometry2.PlatformImpl).Geometry; + var g1 = ((GeometryImpl)geometry1).Geometry; + var g2 = ((GeometryImpl)geometry2).Geometry; var dest = new PathGeometry(Direct2D1Platform.Direct2D1Factory); using var sink = dest.Open(); g1.Combine(g2, (CombineMode)combineMode, sink); diff --git a/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs index 352708bf03..5e49ef6d4e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Platform; using SharpDX.Direct2D1; using AM = Avalonia.Media; @@ -12,19 +13,19 @@ namespace Avalonia.Direct2D1.Media /// /// Initializes a new instance of the class. /// - public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList geometry) + public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList geometry) : base(CreateGeometry(fillRule, geometry)) { } - private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList children) + private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList children) { var count = children.Count; var c = new Geometry[count]; for (var i = 0; i < count; ++i) { - c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry; + c[i] = ((GeometryImpl)children[i]).Geometry; } return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c); diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 481b98a0b2..d494c47a55 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -30,12 +30,12 @@ namespace Avalonia.Base.UnitTests.VisualTree return new MockStreamGeometry(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { throw new NotImplementedException(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index e5cbae4ae7..d40abd9f47 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -32,12 +32,12 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { throw new NotImplementedException(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index df128b8ae3..720755f2b0 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -72,12 +72,12 @@ namespace Avalonia.UnitTests return new MockStreamGeometryImpl(); } - public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) { return Mock.Of(); } - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) + public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) { return Mock.Of(); } From cd5307aa8c4a4ae08e7e2e808617731180527e3c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Mar 2023 18:36:23 +0200 Subject: [PATCH 6/9] Added failing test. --- .../AvaloniaObjectTests_Inheritance.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs index c5cbda5325..707e1d4470 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs @@ -1,5 +1,10 @@ using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Controls.Documents; using Avalonia.Data; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests @@ -90,6 +95,31 @@ namespace Avalonia.Base.UnitTests Assert.Equal(1, raised); } + [Fact] + public void ClearValue_On_Parent_Raises_PropertyChanged_On_Child_With_Inherited_Grandparent_Value() + { + var grandparent = new Class1(); + var parent = new Class2 { Parent = grandparent }; + var child = new Class2 { Parent = parent }; + var raised = 0; + + grandparent.SetValue(Class1.BazProperty, "grandparent"); + parent.SetValue(Class1.BazProperty, "parent"); + + child.PropertyChanged += (s, e) => + { + Assert.Same(child, e.Sender); + Assert.Equal("parent", e.OldValue); + Assert.Equal("grandparent", e.NewValue); + Assert.Equal(BindingPriority.Inherited, e.Priority); + ++raised; + }; + + parent.ClearValue(Class1.BazProperty); + + Assert.Equal(1, raised); + } + [Fact] public void Setting_InheritanceParent_Raises_PropertyChanged_When_Parent_Has_Value_Set() { From 683afe6e9fdfa265e44289d3f76dc297f117584c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Mar 2023 18:45:48 +0200 Subject: [PATCH 7/9] Use correct newValue on inherited property change. - `oldValue` was misnamed - it's the new value - We calculated the new value and then didn't pass it to children for their property changed events: they were using the default value which may be wrong if the value is set further up the tree --- src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs | 12 ++++++------ src/Avalonia.Base/PropertyStore/ValueStore.cs | 8 +++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs index 330034f51d..6f0f6702e5 100644 --- a/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs +++ b/src/Avalonia.Base/PropertyStore/EffectiveValue`1.cs @@ -140,24 +140,24 @@ namespace Avalonia.PropertyStore var p = (StyledProperty)property; BindingPriority priority; - T oldValue; + T newValue; if (property.Inherits && owner.TryGetInheritedValue(property, out var i)) { - oldValue = ((EffectiveValue)i).Value; + newValue = ((EffectiveValue)i).Value; priority = BindingPriority.Inherited; } else { - oldValue = _metadata.DefaultValue; + newValue = _metadata.DefaultValue; priority = BindingPriority.Unset; } - if (!EqualityComparer.Default.Equals(oldValue, Value)) + if (!EqualityComparer.Default.Equals(newValue, Value)) { - owner.Owner.RaisePropertyChanged(p, Value, oldValue, priority, true); + owner.Owner.RaisePropertyChanged(p, Value, newValue, priority, true); if (property.Inherits) - owner.OnInheritedEffectiveValueDisposed(p, Value); + owner.OnInheritedEffectiveValueDisposed(p, Value, newValue); } if (ValueEntry?.GetDataValidationState(out _, out _) ?? diff --git a/src/Avalonia.Base/PropertyStore/ValueStore.cs b/src/Avalonia.Base/PropertyStore/ValueStore.cs index af31459a98..2b5a2f5022 100644 --- a/src/Avalonia.Base/PropertyStore/ValueStore.cs +++ b/src/Avalonia.Base/PropertyStore/ValueStore.cs @@ -419,7 +419,9 @@ namespace Avalonia.PropertyStore ReevaluateEffectiveValue(property, current); } else + { ReevaluateEffectiveValues(); + } } /// @@ -481,7 +483,8 @@ namespace Avalonia.PropertyStore /// /// The property whose value changed. /// The old value of the property. - public void OnInheritedEffectiveValueDisposed(StyledProperty property, T oldValue) + /// The new value of the property. + public void OnInheritedEffectiveValueDisposed(StyledProperty property, T oldValue, T newValue) { Debug.Assert(property.Inherits); @@ -489,12 +492,11 @@ namespace Avalonia.PropertyStore if (children is not null) { - var defaultValue = property.GetDefaultValue(Owner.GetType()); var count = children.Count; for (var i = 0; i < count; ++i) { - children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, defaultValue); + children[i].GetValueStore().OnAncestorInheritedValueChanged(property, oldValue, newValue); } } } From 668fa942b70bd36279e980bd58c094c848a316d1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Mar 2023 22:28:55 +0200 Subject: [PATCH 8/9] Don't leak IPlatformRenderInterface. --- .../Media/GlyphRunTests.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 43feb75c08..84ce341e98 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -1,22 +1,13 @@ using System; -using System.Linq; using Avalonia.Media; using Avalonia.Media.TextFormatting; -using Avalonia.Platform; using Avalonia.UnitTests; -using Avalonia.Utilities; using Xunit; namespace Avalonia.Base.UnitTests.Media { public class GlyphRunTests : TestWithServicesBase { - public GlyphRunTests() - { - AvaloniaLocator.CurrentMutable - .Bind().ToSingleton(); - } - [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0, 0, 0)] [InlineData(new double[] { 30, 0, 0 }, new int[] { 0, 0, 0 }, 0, 3, 30)] [InlineData(new double[] { 10, 10, 10 }, new int[] { 0, 1, 2 }, 1, 0, 10)] @@ -25,7 +16,7 @@ namespace Avalonia.Base.UnitTests.Media [Theory] public void Should_Get_Distance_From_CharacterHit(double[] advances, int[] clusters, int start, int trailingLength, double expectedDistance) { - using(UnitTestApplication.Start(TestServices.StyledWindow)) + using (Start()) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var characterHit = new CharacterHit(start, trailingLength); @@ -44,7 +35,7 @@ namespace Avalonia.Base.UnitTests.Media public void Should_Get_CharacterHit_FromDistance(double[] advances, int[] clusters, double distance, int start, int trailingLengthExpected, bool isInsideExpected) { - using(UnitTestApplication.Start(TestServices.StyledWindow)) + using (Start()) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside); @@ -190,5 +181,11 @@ namespace Avalonia.Base.UnitTests.Media return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, biDiLevel: bidiLevel); } + + private static IDisposable Start() + { + return UnitTestApplication.Start(TestServices.StyledWindow.With( + renderInterface: new MockPlatformRenderInterface())); + } } } From a28483252b9039d5e9c921eb054c3aa034963d33 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Mar 2023 22:29:48 +0200 Subject: [PATCH 9/9] Make MaskedTextBox tests pass again. They need a registered `IPlatformRenderInterface` and were only passing because `GlyphRunTests` was leaking one. --- tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index 19009416ef..a51abdb9af 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -109,7 +109,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void CaretIndex_Can_Moved_To_Position_After_The_End_Of_Text_With_Arrow_Key() { - using (Start(TestServices.StyledWindow)) + using (Start()) { var target = new MaskedTextBox { @@ -184,7 +184,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() { - using (Start(TestServices.StyledWindow)) + using (Start()) { MaskedTextBox textBox = new MaskedTextBox { @@ -226,7 +226,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Control_Delete_Should_Remove_The_Word_After_The_Caret_If_There_Is_No_Selection() { - using (Start(TestServices.StyledWindow)) + using (Start()) { var textBox = new MaskedTextBox { @@ -812,7 +812,7 @@ namespace Avalonia.Controls.UnitTests bool fromClipboard, string expected) { - using (Start(TestServices.StyledWindow)) + using (Start()) { var target = new MaskedTextBox { @@ -898,6 +898,7 @@ namespace Avalonia.Controls.UnitTests standardCursorFactory: Mock.Of()); private static TestServices Services => TestServices.MockThreadingInterface.With( + renderInterface: new MockPlatformRenderInterface(), standardCursorFactory: Mock.Of(), textShaperImpl: new MockTextShaperImpl(), fontManagerImpl: new MockFontManagerImpl());